mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
331 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e05bc269e6 | |||
| d574341f1f | |||
| 481958f8f7 | |||
| 4094469d59 | |||
| 2261fcb631 | |||
| 279c8b6aa2 | |||
| e35c630c1d | |||
| d3047afa7f | |||
| a03783f54c | |||
| cbf0d6190d | |||
| 89c991b636 | |||
| bbbd35e9ef | |||
| c308be315d | |||
| d825e3125e | |||
| 64288de04e | |||
| fb4471e69d | |||
| 8be8694f5f | |||
| 60b78dc2cd | |||
| 80fe5a8167 | |||
| df58c49876 | |||
| 7dee2f6995 | |||
| 623687e59b | |||
| 5958d3be62 | |||
| 142e57450d | |||
| 80815a1591 | |||
| 8412bfb813 | |||
| a0f279691a | |||
| 92aeddb9fe | |||
| d7da88853b | |||
| 89678c7b1e | |||
| 098c826095 | |||
| befbdc3ae5 | |||
| dca0364f4c | |||
| 37771259d9 | |||
| 4618e4851c | |||
| b2ca280c49 | |||
| bf6995f759 | |||
| ab0cce7cb7 | |||
| 2e422fc026 | |||
| a2f9d132a0 | |||
| 1973b97cc2 | |||
| b3c6f0e661 | |||
| 6998b17f9e | |||
| ed9932d70d | |||
| a5f3b2a949 | |||
| 152ed59502 | |||
| 8e16be9e11 | |||
| 300701f44e | |||
| d1370622d8 | |||
| 0134166009 | |||
| ddb9084260 | |||
| 0224452cef | |||
| c17d4dc050 | |||
| 4e33f45522 | |||
| b16d7abb35 | |||
| 2f17a30157 | |||
| 0dbd14ebdc | |||
| 8b3d8ccb47 | |||
| f8ff2e4e28 | |||
| 044f0d41a5 | |||
| 4089bebd83 | |||
| d4787c75fd | |||
| 3bf0a57b82 | |||
| cc505ae49f | |||
| 2f6de136dd | |||
| da21b50137 | |||
| a38a5c529f | |||
| 44b5612697 | |||
| 0113292cf6 | |||
| 4741ab2e04 | |||
| 08fb9435fd | |||
| 793e92e9d6 | |||
| a7c57f4faf | |||
| 8409107a5b | |||
| 9089c3fb02 | |||
| 6c897d5201 | |||
| 6cb5135f34 | |||
| 44bf45794e | |||
| d6da9f47d8 | |||
| be05b66ac3 | |||
| d1998ae3fa | |||
| 3c2e1554c6 | |||
| 744955ba69 | |||
| 7af33f9e6a | |||
| 3c0705b0ae | |||
| 4ea4d2bd3b | |||
| 6c52077d92 | |||
| 73bf7b1730 | |||
| b394cb6379 | |||
| 60854e180e | |||
| 5b4750a009 | |||
| ad50dd21fe | |||
| 8b0cb0bb57 | |||
| a24a6e4e3c | |||
| 6fba4c371e | |||
| 27911431db | |||
| db6447ed79 | |||
| 99c0fabee6 | |||
| fc99724aba | |||
| 88fbc62b1d | |||
| e8027d571f | |||
| daaee43be3 | |||
| 0d71cb93af | |||
| e5e50e82d5 | |||
| 7e852124a5 | |||
| f66a49bc42 | |||
| baf78ccda2 | |||
| 31f0e66f45 | |||
| 28b78a563b | |||
| 2f380de73b | |||
| e3a9a39c9a | |||
| 1710bb78df | |||
| 3e13fc3e70 | |||
| befc399506 | |||
| 88116b9fb1 | |||
| 53e1c58cc5 | |||
| 4b9ecdd11d | |||
| e31e409ee8 | |||
| 5488aaf69f | |||
| 96e493d8b1 | |||
| e409453fbd | |||
| 309bf1348c | |||
| 76a5635298 | |||
| f4f2a1f6de | |||
| a440805ea1 | |||
| c359672bd2 | |||
| 38350935e6 | |||
| 421cd89a0f | |||
| 5ce3369aa6 | |||
| f38acfe988 | |||
| 965619d096 | |||
| 9f017e834c | |||
| 3c67b08488 | |||
| 4add755a4d | |||
| 56e249aee6 | |||
| 6a7c8fcfd5 | |||
| 14b1003c62 | |||
| 43a4bae010 | |||
| 9c205f77a2 | |||
| c2e4cfd832 | |||
| c008e1c5bc | |||
| 1aa60f0da3 | |||
| bd1fd8383c | |||
| aac54d0ea1 | |||
| 4fe718581b | |||
| 71842f07bd | |||
| f2bec1f82f | |||
| 10460191b9 | |||
| c5fffd6e2c | |||
| 951f63b6fd | |||
| e6d8932b3b | |||
| 70f96cca0a | |||
| 4e357e9659 | |||
| 1f8aed6732 | |||
| fa2bace3cd | |||
| 955039b5ea | |||
| 771ea1e815 | |||
| d38bfc4aff | |||
| fbb0054232 | |||
| 2d3c36edae | |||
| 8dcc41a54d | |||
| ba3d2e36c8 | |||
| b51047ffcc | |||
| b1c40a9079 | |||
| b014c267ae | |||
| 6b16cc52db | |||
| d35ad73e35 | |||
| 2a1af3d9ae | |||
| 82e30246c1 | |||
| bb3a05bb3f | |||
| 40fa82275c | |||
| 9824321fc9 | |||
| 27e607ab82 | |||
| a2b27b8790 | |||
| 396089ef0e | |||
| df98b5021d | |||
| 34ce6d0b02 | |||
| 7af937b08e | |||
| 8665003269 | |||
| 1e76716819 | |||
| 91a42fdf58 | |||
| 5ed5243be6 | |||
| 4560251e64 | |||
| 2020dca3e0 | |||
| 7fc2121454 | |||
| 8b84afbd38 | |||
| 305fc3b557 | |||
| 61f2ac01d7 | |||
| 39a9f55205 | |||
| 11f351dbeb | |||
| 815fa379ea | |||
| 4c480a1ea3 | |||
| fa4aa0e06d | |||
| e2a6374bf5 | |||
| dc14554053 | |||
| 985ca7b643 | |||
| 60624d64fa | |||
| 2935dae89e | |||
| 4c22c3285d | |||
| 93cee2994a | |||
| 9c7e8d04d2 | |||
| 1e6b8906e0 | |||
| 6c5b92e5c0 | |||
| 38c515e12e | |||
| c239937fac | |||
| bafa574784 | |||
| 199a5854a8 | |||
| a74a578198 | |||
| 7de752ec56 | |||
| 0a833171ac | |||
| 1a0612cbfd | |||
| fbbd3ba349 | |||
| 1028639186 | |||
| 0e5e764c78 | |||
| db1faecc95 | |||
| c2c415d2e8 | |||
| d193928f31 | |||
| 17861e0003 | |||
| 97fe964e00 | |||
| 9debb5db23 | |||
| 494b438151 | |||
| 010a236882 | |||
| 1951d2a9f2 | |||
| 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 |
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
smithay:
|
||||
patterns:
|
||||
- "smithay"
|
||||
- "smithay-drm-extras"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
+23
-27
@@ -23,8 +23,7 @@ jobs:
|
||||
release-flag: '--release'
|
||||
|
||||
name: test - ${{ matrix.configuration }}
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -33,8 +32,8 @@ 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 libdisplay-info-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -63,7 +62,7 @@ jobs:
|
||||
- name: Build (with profiling)
|
||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
||||
|
||||
- name: Build Tests
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||
|
||||
- name: Test
|
||||
@@ -74,8 +73,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -84,8 +82,8 @@ 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 libdisplay-info-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -98,9 +96,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.77.0'
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
name: 'msrv - 1.80.1'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -109,10 +106,10 @@ 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 libdisplay-info-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.77.0
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -123,8 +120,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
name: clippy
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -133,8 +129,8 @@ 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 libdisplay-info-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -146,7 +142,7 @@ jobs:
|
||||
run: cargo clippy --all --all-targets
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -161,8 +157,8 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-22.04
|
||||
container: fedora:39
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -172,13 +168,13 @@ 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 libdisplay-info-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 libdisplay-info-devel libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build --all
|
||||
|
||||
nix:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -192,7 +188,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:
|
||||
@@ -200,7 +196,7 @@ jobs:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -212,7 +208,7 @@ jobs:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Prepare release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Public version'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Create vendored dependencies archive
|
||||
run: |
|
||||
mkdir .cargo
|
||||
cargo vendor --locked > .cargo/config.toml
|
||||
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --frozen --release
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --frozen --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --frozen --release -- --nocapture
|
||||
|
||||
- name: Draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
|
||||
fail_on_unmatched_files: true
|
||||
Generated
+974
-1067
File diff suppressed because it is too large
Load Diff
+52
-40
@@ -1,25 +1,29 @@
|
||||
[workspace]
|
||||
members = ["niri-visual-tests"]
|
||||
members = [
|
||||
"niri-config",
|
||||
"niri-ipc",
|
||||
"niri-visual-tests",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.9"
|
||||
version = "25.1.0"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.77"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.88"
|
||||
bitflags = "2.6.0"
|
||||
clap = { version = "4.5.17", features = ["derive"] }
|
||||
k9 = "0.12.0"
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
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.3", default-features = false }
|
||||
anyhow = "1.0.95"
|
||||
bitflags = "2.7.0"
|
||||
clap = { version = "4.5.26", features = ["derive"] }
|
||||
insta = "1.42.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.0", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
@@ -47,45 +51,44 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.3.1"
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.18.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
|
||||
bytemuck = { version = "1.21.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
drm-ffi = "0.8.0"
|
||||
fastrand = "2.1.1"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.29.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.158"
|
||||
libdisplay-info = "0.1.0"
|
||||
libc = "0.2.169"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.9", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "~4.10.0", optional = true }
|
||||
ordered-float = "4.2.2"
|
||||
pango = { version = "0.20.1", features = ["v1_44"] }
|
||||
pangocairo = "0.20.1"
|
||||
niri-config = { version = "25.1.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.1.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "4.6.0"
|
||||
pango = { version = "0.20.7", features = ["v1_44"] }
|
||||
pangocairo = "0.20.7"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.13"
|
||||
portable-atomic = { version = "1.7.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.15"
|
||||
sd-notify = "0.4.2"
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.10.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 }
|
||||
url = { version = "2.5.4", optional = true }
|
||||
wayland-backend = "0.3.7"
|
||||
wayland-scanner = "0.31.5"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "~3.15.2", optional = true }
|
||||
zbus = { version = "5.2.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -107,15 +110,18 @@ features = [
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
k9.workspace = true
|
||||
proptest = "1.5.0"
|
||||
proptest-derive = "0.5.0"
|
||||
xshell = "0.2.6"
|
||||
calloop-wayland-source = "0.4.0"
|
||||
insta.workspace = true
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||
rayon = "1.10.0"
|
||||
wayland-client = "0.31.7"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||
dbus = ["zbus", "async-io", "notify-rust", "url"]
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
@@ -124,6 +130,8 @@ xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
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 = []
|
||||
|
||||
@@ -136,8 +144,12 @@ lto = "thin"
|
||||
# knuffel with chomsky generates a metric ton of debuginfo.
|
||||
debug = false
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.9"
|
||||
version = "25.01"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">niri</h1>
|
||||
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||
<p align="center">
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||
</p>
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -28,7 +28,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
|
||||
## Features
|
||||
|
||||
- Scrollable tiling
|
||||
- Built from the ground up for scrollable tiling
|
||||
- Dynamic workspaces like in GNOME
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
@@ -45,10 +45,35 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
|
||||
|
||||
## Status
|
||||
|
||||
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.
|
||||
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
|
||||
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
|
||||
|
||||
Give it a try!
|
||||
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
|
||||
Here are some points you may have questions about:
|
||||
|
||||
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
|
||||
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
|
||||
- **NVIDIA**: seems to work fine.
|
||||
- **Floating windows**: yes, starting from niri 25.01.
|
||||
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
|
||||
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
|
||||
We have touchpad gestures, but no touchscreen gestures yet.
|
||||
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
|
||||
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
|
||||
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
|
||||
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
|
||||
- Steam and games, including Proton: work perfectly through xwayland-satellite.
|
||||
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
|
||||
- Discord and other Electron apps: work well through xwayland-satellite.
|
||||
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
|
||||
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
|
||||
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
|
||||
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||
|
||||
## Inspiration
|
||||
|
||||
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
||||
@@ -78,3 +103,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[gamescope]: https://github.com/ValveSoftware/gamescope
|
||||
|
||||
Generated
+21
-95
@@ -1,72 +1,12 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724533099,
|
||||
"narHash": "sha256-ZIDtvVQHoCkNoBlLUB3wmqbqCb0Es3DfdUGeDI/58aY=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7543c8d76f91b8844e0f3b3cc347a72d8fb4b49e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1722493751,
|
||||
"narHash": "sha256-l7/yMehbrL5d4AI8E2hKtNlT50BlUAau4EKTgPg9KcY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "60ab4a085ef6ee40f2ef7921ca4061084dd8cf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "monthly",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1710156097,
|
||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
||||
"lastModified": 1731533336,
|
||||
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
||||
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1724395761,
|
||||
"narHash": "sha256-zRkDV/nbrnp3Y8oCADf5ETl1sDrdmAW6/bBVJ8EbIdQ=",
|
||||
"lastModified": 1733064805,
|
||||
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ae815cee91b417be55d43781eb4b73ae1ecc396c",
|
||||
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
|
||||
"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": 1722449213,
|
||||
"narHash": "sha256-1na4m2PNH99syz2g/WQ+Hr3RfY7k4H8NBnmkr5dFDXw=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c8e41d95061543715b30880932ec3dc24c42d7ae",
|
||||
"lastModified": 1733106880,
|
||||
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,101 +4,251 @@
|
||||
|
||||
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.mkLib pkgs).overrideToolchain toolchain;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-filter,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
cairo,
|
||||
dbus,
|
||||
libGL,
|
||||
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; [
|
||||
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 = [
|
||||
rustPlatform.bindgenHook
|
||||
pkg-config
|
||||
autoPatchelfHook
|
||||
clang
|
||||
gdk-pixbuf
|
||||
graphene
|
||||
gtk4
|
||||
libadwaita
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
wayland
|
||||
systemd # For libudev
|
||||
seatd # For libseat
|
||||
libxkbcommon
|
||||
libdisplay-info
|
||||
libinput
|
||||
mesa # For libgbm
|
||||
fontconfig
|
||||
stdenv.cc.cc.lib
|
||||
pipewire
|
||||
pango
|
||||
cairo
|
||||
glib
|
||||
pixman
|
||||
];
|
||||
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
|
||||
libxkbcommon
|
||||
];
|
||||
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";
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath craneArgs.runtimeDependencies; # Needed for tests to find libxkbcommon
|
||||
# ever since this commit:
|
||||
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
|
||||
# and thus creates a real socket file in the runtime dir.
|
||||
# this is fine for our build, we just need to make sure it has a directory to write to.
|
||||
preCheck = ''
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
'';
|
||||
|
||||
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 = {
|
||||
# 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 = craneLib.devShell {
|
||||
inputsFrom = [niri];
|
||||
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"
|
||||
];
|
||||
}
|
||||
))
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (craneArgs.runtimeDependencies ++ craneArgs.nativeBuildInputs ++ craneArgs.buildInputs);
|
||||
inherit (niri) LIBCLANG_PATH;
|
||||
};
|
||||
}
|
||||
);
|
||||
nativeBuildInputs = [
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
pkgs.pkg-config
|
||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
buildInputs = niri.buildInputs ++ [
|
||||
pkgs.libadwaita # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
env = {
|
||||
# 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 { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ bitflags.workspace = true
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
niri-ipc = { version = "0.1.9", path = "../niri-ipc" }
|
||||
regex = "1.10.6"
|
||||
niri-ipc = { version = "25.1.0", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
k9.workspace = true
|
||||
insta.workspace = true
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::{BlockOutFrom, RegexEq};
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
#[knuffel(children(name = "match"))]
|
||||
pub matches: Vec<Match>,
|
||||
#[knuffel(children(name = "exclude"))]
|
||||
pub excludes: Vec<Match>,
|
||||
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub opacity: Option<f32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct Match {
|
||||
#[knuffel(property, str)]
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
}
|
||||
+506
-45
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
/// `Regex` that implements `PartialEq` by its string form.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegexEq(pub Regex);
|
||||
|
||||
impl PartialEq for RegexEq {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_str() == other.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RegexEq {}
|
||||
|
||||
impl FromStr for RegexEq {
|
||||
type Err = <Regex as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Regex::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,12 +1,16 @@
|
||||
[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 }
|
||||
|
||||
@@ -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 = "=25.1.0"
|
||||
```
|
||||
+263
-6
@@ -19,6 +19,20 @@
|
||||
//!
|
||||
//! 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 = "=25.1.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## 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;
|
||||
@@ -41,6 +55,8 @@ pub enum Request {
|
||||
Workspaces,
|
||||
/// Request information about open windows.
|
||||
Windows,
|
||||
/// Request information about layer-shell surfaces.
|
||||
Layers,
|
||||
/// Request information about the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Request information about the focused output.
|
||||
@@ -105,6 +121,8 @@ pub enum Response {
|
||||
Workspaces(Vec<Workspace>),
|
||||
/// Information about open windows.
|
||||
Windows(Vec<Window>),
|
||||
/// Information about layer-shell surfaces.
|
||||
Layers(Vec<LayerSurface>),
|
||||
/// Information about the keyboard layout.
|
||||
KeyboardLayouts(KeyboardLayouts),
|
||||
/// Information about the focused output.
|
||||
@@ -132,6 +150,8 @@ pub enum Action {
|
||||
},
|
||||
/// Power off all monitors via DPMS.
|
||||
PowerOffMonitors {},
|
||||
/// Power on all monitors via DPMS.
|
||||
PowerOnMonitors {},
|
||||
/// Spawn a command.
|
||||
Spawn {
|
||||
/// Command to spawn.
|
||||
@@ -184,6 +204,8 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
/// Focus the previously focused window.
|
||||
FocusWindowPrevious {},
|
||||
/// Focus the column to the left.
|
||||
FocusColumnLeft {},
|
||||
/// Focus the column to the right.
|
||||
@@ -240,16 +262,52 @@ pub enum Action {
|
||||
MoveWindowDownOrToWorkspaceDown {},
|
||||
/// Move the focused window up in a column or to the workspace above.
|
||||
MoveWindowUpOrToWorkspaceUp {},
|
||||
/// Consume or expel the focused window left.
|
||||
ConsumeOrExpelWindowLeft {},
|
||||
/// Consume or expel the focused window right.
|
||||
ConsumeOrExpelWindowRight {},
|
||||
/// Consume 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 {},
|
||||
/// Expel the focused window from the column.
|
||||
ExpelWindowFromColumn {},
|
||||
/// Swap focused window with one to the right
|
||||
SwapWindowRight {},
|
||||
/// Swap focused window with one to the left
|
||||
SwapWindowLeft {},
|
||||
/// Center the focused column on the screen.
|
||||
CenterColumn {},
|
||||
/// Center a window on the screen.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Center the focused window on the screen")
|
||||
)]
|
||||
CenterWindow {
|
||||
/// Id of the window to center.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Focus the workspace below.
|
||||
FocusWorkspaceDown {},
|
||||
/// Focus the workspace above.
|
||||
@@ -296,6 +354,34 @@ pub enum Action {
|
||||
MoveWorkspaceDown {},
|
||||
/// Move the focused workspace up.
|
||||
MoveWorkspaceUp {},
|
||||
/// Set the name of a workspace.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Set the name of the focused workspace")
|
||||
)]
|
||||
SetWorkspaceName {
|
||||
/// New name for the workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
name: String,
|
||||
|
||||
/// Reference (index or name) of the workspace to name.
|
||||
///
|
||||
/// If `None`, uses the focused workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
workspace: Option<WorkspaceReferenceArg>,
|
||||
},
|
||||
/// Unset the name of a workspace.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Unset the name of the focused workspace")
|
||||
)]
|
||||
UnsetWorkspaceName {
|
||||
/// Reference (index or name) of the workspace to unname.
|
||||
///
|
||||
/// If `None`, uses the focused workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
reference: Option<WorkspaceReferenceArg>,
|
||||
},
|
||||
/// Focus the monitor to the left.
|
||||
FocusMonitorLeft {},
|
||||
/// Focus the monitor to the right.
|
||||
@@ -304,6 +390,10 @@ pub enum Action {
|
||||
FocusMonitorDown {},
|
||||
/// Focus the monitor above.
|
||||
FocusMonitorUp {},
|
||||
/// Focus the previous monitor.
|
||||
FocusMonitorPrevious {},
|
||||
/// Focus the next monitor.
|
||||
FocusMonitorNext {},
|
||||
/// Move the focused window to the monitor to the left.
|
||||
MoveWindowToMonitorLeft {},
|
||||
/// Move the focused window to the monitor to the right.
|
||||
@@ -312,6 +402,10 @@ pub enum Action {
|
||||
MoveWindowToMonitorDown {},
|
||||
/// Move the focused window to the monitor above.
|
||||
MoveWindowToMonitorUp {},
|
||||
/// Move the focused window to the previous monitor.
|
||||
MoveWindowToMonitorPrevious {},
|
||||
/// Move the focused window to the next monitor.
|
||||
MoveWindowToMonitorNext {},
|
||||
/// Move the focused column to the monitor to the left.
|
||||
MoveColumnToMonitorLeft {},
|
||||
/// Move the focused column to the monitor to the right.
|
||||
@@ -320,6 +414,26 @@ pub enum Action {
|
||||
MoveColumnToMonitorDown {},
|
||||
/// Move the focused column to the monitor above.
|
||||
MoveColumnToMonitorUp {},
|
||||
/// Move the focused column to the previous monitor.
|
||||
MoveColumnToMonitorPrevious {},
|
||||
/// Move the focused column to the next monitor.
|
||||
MoveColumnToMonitorNext {},
|
||||
/// Change the width of a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Change the width of the focused window")
|
||||
)]
|
||||
SetWindowWidth {
|
||||
/// Id of the window whose width to set.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
|
||||
/// How to change the width.
|
||||
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Change the height of a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
@@ -333,7 +447,7 @@ pub enum Action {
|
||||
id: Option<u64>,
|
||||
|
||||
/// How to change the height.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Reset the height of a window back to automatic.
|
||||
@@ -350,6 +464,14 @@ pub enum Action {
|
||||
},
|
||||
/// Switch between preset column widths.
|
||||
SwitchPresetColumnWidth {},
|
||||
/// Switch between preset window widths.
|
||||
SwitchPresetWindowWidth {
|
||||
/// Id of the window whose width to switch.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Switch between preset window heights.
|
||||
SwitchPresetWindowHeight {
|
||||
/// Id of the window whose height to switch.
|
||||
@@ -363,7 +485,7 @@ pub enum Action {
|
||||
/// Change the width of the focused column.
|
||||
SetColumnWidth {
|
||||
/// How to change the width.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Switch between keyboard layouts.
|
||||
@@ -382,12 +504,69 @@ pub enum Action {
|
||||
MoveWorkspaceToMonitorDown {},
|
||||
/// Move the focused workspace to the monitor above.
|
||||
MoveWorkspaceToMonitorUp {},
|
||||
/// Move the focused workspace to the previous monitor.
|
||||
MoveWorkspaceToMonitorPrevious {},
|
||||
/// Move the focused workspace to the next monitor.
|
||||
MoveWorkspaceToMonitorNext {},
|
||||
/// Toggle a debug tint on windows.
|
||||
ToggleDebugTint {},
|
||||
/// Toggle visualization of render element opaque regions.
|
||||
DebugToggleOpaqueRegions {},
|
||||
/// Toggle visualization of output damage.
|
||||
DebugToggleDamage {},
|
||||
/// Move the focused window between the floating and the tiling layout.
|
||||
ToggleWindowFloating {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Move the focused window to the floating layout.
|
||||
MoveWindowToFloating {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Move the focused window to the tiling layout.
|
||||
MoveWindowToTiling {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Switches focus to the floating layout.
|
||||
FocusFloating {},
|
||||
/// Switches focus to the tiling layout.
|
||||
FocusTiling {},
|
||||
/// Toggles the focus between the floating and the tiling layout.
|
||||
SwitchFocusBetweenFloatingAndTiling {},
|
||||
/// Move a floating window on screen.
|
||||
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
|
||||
MoveFloatingWindow {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
|
||||
/// How to change the X position.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
arg(short, long, default_value = "+0", allow_negative_numbers = true)
|
||||
)]
|
||||
x: PositionChange,
|
||||
|
||||
/// How to change the Y position.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
arg(short, long, default_value = "+0", allow_negative_numbers = true)
|
||||
)]
|
||||
y: PositionChange,
|
||||
},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
@@ -404,6 +583,16 @@ pub enum SizeChange {
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
/// Change in floating window position.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum PositionChange {
|
||||
/// Set the position in logical pixels.
|
||||
SetFixed(f64),
|
||||
/// Add or subtract to the current position in logical pixels.
|
||||
AdjustFixed(f64),
|
||||
}
|
||||
|
||||
/// Workspace reference (id, index or name) to operate on.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -659,12 +848,21 @@ pub struct Window {
|
||||
pub title: Option<String>,
|
||||
/// Application ID, if set.
|
||||
pub app_id: Option<String>,
|
||||
/// Process ID that created the Wayland connection for this window, if known.
|
||||
///
|
||||
/// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
|
||||
/// change in the future.
|
||||
pub pid: Option<i32>,
|
||||
/// 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,
|
||||
/// Whether this window is currently floating.
|
||||
///
|
||||
/// If the window isn't floating then it is in the tiling layout.
|
||||
pub is_floating: bool,
|
||||
}
|
||||
|
||||
/// Output configuration change result.
|
||||
@@ -726,6 +924,46 @@ pub struct KeyboardLayouts {
|
||||
pub current_idx: u8,
|
||||
}
|
||||
|
||||
/// A layer-shell layer.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Layer {
|
||||
/// The background layer.
|
||||
Background,
|
||||
/// The bottom layer.
|
||||
Bottom,
|
||||
/// The top layer.
|
||||
Top,
|
||||
/// The overlay layer.
|
||||
Overlay,
|
||||
}
|
||||
|
||||
/// Keyboard interactivity modes for a layer-shell surface.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum LayerSurfaceKeyboardInteractivity {
|
||||
/// Surface cannot receive keyboard focus.
|
||||
None,
|
||||
/// Surface receives keyboard focus whenever possible.
|
||||
Exclusive,
|
||||
/// Surface receives keyboard focus on demand, e.g. when clicked.
|
||||
OnDemand,
|
||||
}
|
||||
|
||||
/// A layer-shell surface.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct LayerSurface {
|
||||
/// Namespace provided by the layer-shell client.
|
||||
pub namespace: String,
|
||||
/// Name of the output the surface is on.
|
||||
pub output: String,
|
||||
/// Layer that the surface is on.
|
||||
pub layer: Layer,
|
||||
/// The surface's keyboard interactivity mode.
|
||||
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// A compositor event.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -855,6 +1093,25 @@ impl FromStr for SizeChange {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PositionChange {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let value = s;
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::AdjustFixed(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::SetFixed(value))
|
||||
}
|
||||
None => Err("value is missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LayoutSwitchTarget {
|
||||
type Err = &'static str;
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.1", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.9", path = ".." }
|
||||
niri-config = { version = "0.1.9", path = "../niri-config" }
|
||||
gtk = { version = "0.9.5", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.1.0", path = ".." }
|
||||
niri-config = { version = "25.1.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::f32::consts::{FRAC_PI_2, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
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};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientAngle {
|
||||
angle: f32,
|
||||
@@ -17,7 +15,7 @@ pub struct GradientAngle {
|
||||
}
|
||||
|
||||
impl GradientAngle {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
angle: 0.,
|
||||
prev_time: Duration::ZERO,
|
||||
@@ -31,20 +29,13 @@ impl TestCase for GradientAngle {
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
let delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.angle += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.angle >= PI * 2. {
|
||||
@@ -59,16 +50,16 @@ impl TestCase for GradientAngle {
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(area.size),
|
||||
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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use std::f32::consts::{FRAC_PI_4, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::layout::focus_ring::FocusRing;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
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};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientArea {
|
||||
progress: f32,
|
||||
@@ -19,7 +17,7 @@ pub struct GradientArea {
|
||||
}
|
||||
|
||||
impl GradientArea {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
let border = FocusRing::new(niri_config::FocusRing {
|
||||
off: false,
|
||||
width: FloatOrInt(1.),
|
||||
@@ -43,20 +41,13 @@ impl TestCase for GradientArea {
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
let delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.progress += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.progress >= PI * 2. {
|
||||
@@ -74,8 +65,8 @@ impl TestCase for GradientArea {
|
||||
let f = (self.progress.sin() + 1.) / 2.;
|
||||
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let rect_size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
|
||||
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
|
||||
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
|
||||
|
||||
let g_size = Size::from((
|
||||
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||
@@ -83,7 +74,7 @@ impl TestCase for GradientArea {
|
||||
));
|
||||
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
|
||||
let g_size = g_size.to_f64();
|
||||
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
|
||||
let mut g_area = Rectangle::new(g_loc, g_size);
|
||||
g_area.loc -= area.loc;
|
||||
|
||||
self.border.update_render_elements(
|
||||
@@ -108,7 +99,7 @@ impl TestCase for GradientArea {
|
||||
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(),
|
||||
Rectangle::from_size(rect_size).to_f64(),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklab {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklab {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklab {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -2,16 +2,16 @@ 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 smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklabAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklabAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
@@ -29,16 +29,16 @@ impl TestCase for GradientOklabAlpha {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchAlpha {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchDecreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchDecreasing {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchDecreasing {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchIncreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchIncreasing {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchIncreasing {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchLonger {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchLonger {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchLonger {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchShorter {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchShorter {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchShorter {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgb {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgb {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientSrgb {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -2,16 +2,16 @@ 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 smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
@@ -29,16 +29,16 @@ impl TestCase for GradientSrgbAlpha {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -4,16 +4,16 @@ use niri_config::{
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinear {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinear {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
@@ -31,16 +31,16 @@ impl TestCase for GradientSrgbLinear {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -2,16 +2,16 @@ 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 smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinearAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinearAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
@@ -29,16 +29,16 @@ impl TestCase for GradientSrgbLinearAlpha {
|
||||
) -> 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();
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
Rectangle::from_size(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),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::workspace::ColumnWidth;
|
||||
use niri::layout::{LayoutElement as _, Options};
|
||||
use niri::animation::Clock;
|
||||
use niri::layout::scrolling::ColumnWidth;
|
||||
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::utils::get_monotonic_time;
|
||||
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;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
@@ -20,13 +20,16 @@ type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
pub struct Layout {
|
||||
output: Output,
|
||||
windows: Vec<TestWindow>,
|
||||
clock: Clock,
|
||||
layout: niri::layout::Layout<TestWindow>,
|
||||
start_time: Duration,
|
||||
steps: HashMap<Duration, DynStepFn>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(args: Args) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
let output = Output::new(
|
||||
String::new(),
|
||||
PhysicalProperties {
|
||||
@@ -63,20 +66,23 @@ impl Layout {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = niri::layout::Layout::with_options(options);
|
||||
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
|
||||
layout.add_output(output.clone());
|
||||
|
||||
let start_time = clock.now_unadjusted();
|
||||
|
||||
Self {
|
||||
output,
|
||||
windows: Vec::new(),
|
||||
clock,
|
||||
layout,
|
||||
start_time: get_monotonic_time(),
|
||||
start_time,
|
||||
steps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_in_between(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
@@ -91,8 +97,8 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_multiple_quickly(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
@@ -105,8 +111,8 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_multiple_quickly_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
@@ -119,8 +125,8 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_to_the_left(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
@@ -135,8 +141,8 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_to_the_left_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
||||
@@ -153,10 +159,24 @@ 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, None);
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::Auto,
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
@@ -167,11 +187,24 @@ impl Layout {
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false, None);
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout
|
||||
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::NextTo(right_of.id()),
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
@@ -198,29 +231,28 @@ 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) {
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
let now_unadjusted = self.clock.now_unadjusted();
|
||||
let run = self
|
||||
.steps
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|delay| self.start_time + *delay <= current_time)
|
||||
.filter(|delay| self.start_time + *delay <= now_unadjusted)
|
||||
.collect::<Vec<_>>();
|
||||
for key in &run {
|
||||
let f = self.steps.remove(key).unwrap();
|
||||
for delay in &run {
|
||||
let now = self.start_time + *delay;
|
||||
self.clock.set_unadjusted(now);
|
||||
self.layout.advance_animations();
|
||||
|
||||
let f = self.steps.remove(delay).unwrap();
|
||||
f(self);
|
||||
}
|
||||
if !run.is_empty() {
|
||||
current_time = get_monotonic_time();
|
||||
}
|
||||
|
||||
self.layout.advance_animations(current_time);
|
||||
self.clock.set_unadjusted(now_unadjusted);
|
||||
self.layout.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -232,8 +264,7 @@ impl TestCase for Layout {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output)
|
||||
.into_iter()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
|
||||
pub mod gradient_angle;
|
||||
pub mod gradient_area;
|
||||
@@ -21,6 +22,11 @@ pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
pub struct Args {
|
||||
pub size: Size<i32, Logical>,
|
||||
pub clock: Clock,
|
||||
}
|
||||
|
||||
pub trait TestCase {
|
||||
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
|
||||
@@ -6,9 +6,9 @@ use niri::render_helpers::RenderTarget;
|
||||
use niri_config::{Color, FloatOrInt};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Tile {
|
||||
@@ -17,53 +17,46 @@ pub struct Tile {
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::freeform(size);
|
||||
pub fn freeform_open(args: Args) -> Self {
|
||||
let mut rv = Self::freeform(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size(size);
|
||||
pub fn fixed_size_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
||||
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn with_window(window: TestWindow) -> Self {
|
||||
pub fn with_window(args: Args, window: TestWindow) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
@@ -77,15 +70,28 @@ impl Tile {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
|
||||
|
||||
let mut tile = niri::layout::tile::Tile::new(
|
||||
window.clone(),
|
||||
size.to_f64(),
|
||||
1.,
|
||||
clock,
|
||||
Rc::new(options),
|
||||
);
|
||||
|
||||
tile.request_tile_size(size.to_f64(), false, None);
|
||||
window.communicate();
|
||||
|
||||
Self { window, tile }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let size = Size::from((width, height)).to_f64();
|
||||
self.tile
|
||||
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
|
||||
.update_config(size, 1., self.tile.options().clone());
|
||||
self.tile.request_tile_size(size, false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
@@ -93,8 +99,8 @@ impl TestCase for Tile {
|
||||
self.tile.are_animations_ongoing()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.tile.advance_animations(current_time);
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
self.tile.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -108,7 +114,7 @@ impl TestCase for Tile {
|
||||
|
||||
self.tile.update(
|
||||
true,
|
||||
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
);
|
||||
self.tile
|
||||
.render(
|
||||
|
||||
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Window {
|
||||
@@ -12,24 +12,24 @@ pub struct Window {
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(size, false, None);
|
||||
window.request_size(args.size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(size, false, None);
|
||||
window.request_size(args.size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(size, false, None);
|
||||
window.request_size(args.size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
extern crate tracing;
|
||||
|
||||
use std::env;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||
use gtk::prelude::{
|
||||
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
||||
};
|
||||
use cases::Args;
|
||||
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
|
||||
use gtk::{gdk, gio, glib};
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay_view::SmithayView;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -66,24 +62,23 @@ fn on_startup(_app: &adw::Application) {
|
||||
|
||||
fn build_ui(app: &adw::Application) {
|
||||
let stack = gtk::Stack::new();
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
|
||||
struct S {
|
||||
stack: gtk::Stack,
|
||||
anim_adjustment: gtk::Adjustment,
|
||||
}
|
||||
|
||||
impl S {
|
||||
fn add<T: TestCase + 'static>(
|
||||
&self,
|
||||
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
title: &str,
|
||||
) {
|
||||
let view = SmithayView::new(make);
|
||||
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
|
||||
let view = SmithayView::new(make, &self.anim_adjustment);
|
||||
self.stack.add_titled(&view, None, title);
|
||||
}
|
||||
}
|
||||
|
||||
let s = S {
|
||||
stack: stack.clone(),
|
||||
anim_adjustment: anim_adjustment.clone(),
|
||||
};
|
||||
|
||||
s.add(Window::freeform, "Freeform Window");
|
||||
@@ -137,9 +132,6 @@ fn build_ui(app: &adw::Application) {
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
anim_adjustment
|
||||
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
|
||||
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||
anim_scale.set_hexpand(true);
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay::utils::Size;
|
||||
|
||||
use crate::cases::TestCase;
|
||||
use crate::cases::{Args, TestCase};
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::ptr::null;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use niri::animation::Clock;
|
||||
use niri::render_helpers::{resources, shaders};
|
||||
use niri::utils::get_monotonic_time;
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -21,7 +23,7 @@ mod imp {
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
||||
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
@@ -30,6 +32,7 @@ mod imp {
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
pub clock: RefCell<Clock>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@@ -125,16 +128,24 @@ mod imp {
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
let frame_clock = self.obj().frame_clock().unwrap();
|
||||
let time = Duration::from_micros(frame_clock.frame_time() as u64);
|
||||
self.clock.borrow_mut().set_unadjusted(time);
|
||||
|
||||
// Create the test case if missing.
|
||||
let mut case = self.test_case.borrow_mut();
|
||||
let case = case.get_or_insert_with(|| {
|
||||
let make = self.make_test_case.get().unwrap();
|
||||
make(Size::from(size))
|
||||
let args = Args {
|
||||
size: Size::from(size),
|
||||
clock: self.clock.borrow().clone(),
|
||||
};
|
||||
make(args)
|
||||
});
|
||||
|
||||
case.advance_animations(get_monotonic_time());
|
||||
case.advance_animations(self.clock.borrow_mut().now());
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
|
||||
|
||||
let elements = unsafe {
|
||||
with_framebuffer_save_restore(renderer, |renderer| {
|
||||
@@ -233,14 +244,32 @@ glib::wrapper! {
|
||||
|
||||
impl SmithayView {
|
||||
pub fn new<T: TestCase + 'static>(
|
||||
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
make_test_case: impl Fn(Args) -> T + 'static,
|
||||
anim_adjustment: >k::Adjustment,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder().build();
|
||||
|
||||
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
|
||||
let make = move |args| Box::new(make_test_case(args)) as Box<dyn TestCase>;
|
||||
let make_test_case = Box::new(make) as _;
|
||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||
|
||||
anim_adjustment.connect_value_changed({
|
||||
let obj = obj.downgrade();
|
||||
move |adj| {
|
||||
if let Some(obj) = obj.upgrade() {
|
||||
let mut clock = obj.imp().clock.borrow_mut();
|
||||
let instantly = adj.value() == 0.0;
|
||||
let rate = if instantly {
|
||||
1.0
|
||||
} else {
|
||||
1.0 / adj.value().max(0.001)
|
||||
};
|
||||
clock.set_rate(rate);
|
||||
clock.set_complete_instantly(instantly);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ impl LayoutElement for TestWindow {
|
||||
self.inner.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().pending_fullscreen = true;
|
||||
}
|
||||
|
||||
@@ -220,6 +220,8 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn set_active_in_column(&mut self, _active: bool) {}
|
||||
|
||||
fn set_floating(&mut self, _floating: bool) {}
|
||||
|
||||
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
@@ -240,6 +242,10 @@ impl LayoutElement for TestWindow {
|
||||
self.inner.borrow().requested_size
|
||||
}
|
||||
|
||||
fn is_child_of(&self, _parent: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn refresh(&self) {}
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules {
|
||||
@@ -259,7 +265,7 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn cancel_interactive_resize(&mut self) {}
|
||||
|
||||
fn update_interactive_resize(&mut self, _serial: Serial) {}
|
||||
fn on_commit(&mut self, _serial: Serial) {}
|
||||
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
|
||||
+9
-7
@@ -33,7 +33,6 @@ 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
|
||||
@@ -41,18 +40,21 @@ SourceLicense: GPL-3.0-or-later
|
||||
# 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 AND (MIT OR Apache-2.0)
|
||||
# MIT OR Apache-2.0
|
||||
# (MIT OR Apache-2.0) AND BSD-3-Clause
|
||||
# (MIT OR Apache-2.0) AND Unicode-3.0
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unicode-3.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: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT) License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
@@ -101,10 +103,6 @@ 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.
|
||||
@@ -113,6 +111,9 @@ 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
|
||||
|
||||
# Set the commit string.
|
||||
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
|
||||
@@ -127,6 +128,7 @@ install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
%cargo_test -- --workspace --exclude niri-visual-tests
|
||||
%endif
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ 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
|
||||
|
||||
@@ -202,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.
|
||||
@@ -238,6 +250,15 @@ window-rule {
|
||||
default-column-width {}
|
||||
}
|
||||
|
||||
// Open the Firefox picture-in-picture player as floating by default.
|
||||
window-rule {
|
||||
// This app-id regular expression will work for both:
|
||||
// - host Firefox (app-id is "firefox")
|
||||
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
|
||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||
open-floating true
|
||||
}
|
||||
|
||||
// Example: block out two password managers from screen capture.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
@@ -250,6 +271,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
|
||||
@@ -422,13 +450,17 @@ binds {
|
||||
// Switches focus between the current and the previous workspace.
|
||||
// Mod+Tab { focus-workspace-previous; }
|
||||
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// There are also commands that consume or expel a single window to the side.
|
||||
// The following binds move the focused window in and out of a column.
|
||||
// If the window is alone, they will consume it into the nearby column to the side.
|
||||
// If the window is already in a column, they will expel it out.
|
||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
// Consume one window from the right to the bottom of the focused column.
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
// Expel the bottom window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
@@ -451,6 +483,10 @@ binds {
|
||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||
|
||||
// Move the focused window between the floating and the tiling layout.
|
||||
Mod+V { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
|
||||
// Actions to switch layouts.
|
||||
// Note: if you uncomment these, make sure you do NOT have
|
||||
// a matching layout switch hotkey configured in xkb options above.
|
||||
@@ -465,6 +501,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
|
||||
@@ -1,3 +1,5 @@
|
||||
[preferred]
|
||||
default=gnome;gtk;
|
||||
org.freedesktop.impl.portal.Access=gtk;
|
||||
org.freedesktop.impl.portal.Notification=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,202 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
/// Shareable lazy clock that can change rate.
|
||||
///
|
||||
/// The clock will fetch the time once and then retain it until explicitly cleared with
|
||||
/// [`Clock::clear`].
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Clock {
|
||||
inner: Rc<RefCell<AdjustableClock>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LazyClock {
|
||||
time: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Clock that can adjust its rate.
|
||||
#[derive(Debug)]
|
||||
struct AdjustableClock {
|
||||
inner: LazyClock,
|
||||
current_time: Duration,
|
||||
last_seen_time: Duration,
|
||||
rate: f64,
|
||||
complete_instantly: bool,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
/// Creates a new clock with the given time.
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
let clock = AdjustableClock::new(LazyClock::with_time(time));
|
||||
Self {
|
||||
inner: Rc::new(RefCell::new(clock)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current time.
|
||||
pub fn now(&self) -> Duration {
|
||||
self.inner.borrow_mut().now()
|
||||
}
|
||||
|
||||
/// Returns the underlying time not adjusted for rate change.
|
||||
pub fn now_unadjusted(&self) -> Duration {
|
||||
self.inner.borrow_mut().inner.now()
|
||||
}
|
||||
|
||||
/// Sets the unadjusted clock time.
|
||||
pub fn set_unadjusted(&mut self, time: Duration) {
|
||||
self.inner.borrow_mut().inner.set(time);
|
||||
}
|
||||
|
||||
/// Clears the stored time so it's re-fetched again next.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.borrow_mut().inner.clear();
|
||||
}
|
||||
|
||||
/// Gets the clock rate.
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.inner.borrow().rate()
|
||||
}
|
||||
|
||||
/// Sets the clock rate.
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.inner.borrow_mut().set_rate(rate);
|
||||
}
|
||||
|
||||
/// Returns whether animations should complete instantly.
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.inner.borrow().should_complete_instantly()
|
||||
}
|
||||
|
||||
/// Sets whether animations should complete instantly.
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.inner.borrow_mut().set_complete_instantly(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Clock {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Clock {}
|
||||
|
||||
impl LazyClock {
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
Self { time: Some(time) }
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.time = None;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, time: Duration) {
|
||||
self.time = Some(time);
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
*self.time.get_or_insert_with(get_monotonic_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl AdjustableClock {
|
||||
pub fn new(mut inner: LazyClock) -> Self {
|
||||
let time = inner.now();
|
||||
Self {
|
||||
inner,
|
||||
current_time: time,
|
||||
last_seen_time: time,
|
||||
rate: 1.,
|
||||
complete_instantly: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.rate = rate.clamp(0., 1000.);
|
||||
}
|
||||
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.complete_instantly
|
||||
}
|
||||
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.complete_instantly = value;
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
let time = self.inner.now();
|
||||
|
||||
if self.last_seen_time == time {
|
||||
return self.current_time;
|
||||
}
|
||||
|
||||
if self.last_seen_time < time {
|
||||
let delta = time - self.last_seen_time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_add(delta);
|
||||
} else {
|
||||
let delta = self.last_seen_time - time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_sub(delta);
|
||||
}
|
||||
|
||||
self.last_seen_time = time;
|
||||
self.current_time
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdjustableClock {
|
||||
fn default() -> Self {
|
||||
Self::new(LazyClock::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frozen_clock() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
assert_eq!(clock.now(), Duration::ZERO);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_change() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
clock.set_rate(0.5);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(50));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(150));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(150));
|
||||
assert_eq!(clock.now(), Duration::from_millis(75));
|
||||
|
||||
clock.set_rate(2.0);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(250));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(250));
|
||||
assert_eq!(clock.now(), Duration::from_millis(275));
|
||||
}
|
||||
}
|
||||
+53
-98
@@ -2,14 +2,12 @@ use std::time::Duration;
|
||||
|
||||
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||
use keyframe::EasingFunction;
|
||||
use portable_atomic::{AtomicF64, Ordering};
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
mod spring;
|
||||
pub use spring::{Spring, SpringParams};
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
mod clock;
|
||||
pub use clock::Clock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation {
|
||||
@@ -23,7 +21,7 @@ pub struct Animation {
|
||||
/// Best effort; not always exactly precise.
|
||||
clamped_duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
clock: Clock,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
@@ -48,11 +46,17 @@ pub enum Curve {
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
|
||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
pub fn new(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
) -> Self {
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / clock.rate().max(0.001);
|
||||
|
||||
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
if config.off {
|
||||
rv.is_off = true;
|
||||
return rv;
|
||||
@@ -71,7 +75,6 @@ impl Animation {
|
||||
}
|
||||
|
||||
let start_time = self.start_time;
|
||||
let current_time = self.current_time;
|
||||
|
||||
match config.kind {
|
||||
niri_config::AnimationKind::Spring(p) => {
|
||||
@@ -83,10 +86,11 @@ impl Animation {
|
||||
initial_velocity: self.initial_velocity,
|
||||
params,
|
||||
};
|
||||
*self = Self::spring(spring);
|
||||
*self = Self::spring(self.clock.clone(), spring);
|
||||
}
|
||||
niri_config::AnimationKind::Easing(p) => {
|
||||
*self = Self::ease(
|
||||
self.clock.clone(),
|
||||
self.from,
|
||||
self.to,
|
||||
self.initial_velocity,
|
||||
@@ -97,7 +101,6 @@ impl Animation {
|
||||
}
|
||||
|
||||
self.start_time = start_time;
|
||||
self.current_time = current_time;
|
||||
}
|
||||
|
||||
/// Restarts the animation using the previous config.
|
||||
@@ -106,11 +109,12 @@ impl Animation {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => Self::ease(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
@@ -124,23 +128,32 @@ impl Animation {
|
||||
initial_velocity: self.initial_velocity,
|
||||
params: spring.params,
|
||||
};
|
||||
Self::spring(spring)
|
||||
Self::spring(self.clock.clone(), spring)
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let threshold = 0.001; // FIXME
|
||||
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
|
||||
Self::decelerate(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
pub fn ease(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
duration_ms: u64,
|
||||
curve: Curve,
|
||||
) -> Self {
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
let kind = Kind::Easing { curve };
|
||||
|
||||
@@ -152,19 +165,15 @@ impl Animation {
|
||||
duration,
|
||||
// Our current curves never overshoot.
|
||||
clamped_duration: duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spring(spring: Spring) -> Self {
|
||||
pub fn spring(clock: Clock, spring: Spring) -> Self {
|
||||
let _span = tracy_client::span!("Animation::spring");
|
||||
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration = spring.duration();
|
||||
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||
let kind = Kind::Spring(spring);
|
||||
@@ -176,22 +185,19 @@ impl Animation {
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decelerate(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
threshold: f64,
|
||||
) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration_s = if initial_velocity == 0. {
|
||||
0.
|
||||
} else {
|
||||
@@ -214,77 +220,26 @@ impl Animation {
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration: duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_time(&mut self, time: Duration) {
|
||||
if self.duration.is_zero() {
|
||||
self.current_time = time;
|
||||
return;
|
||||
}
|
||||
|
||||
let end_time = self.start_time + self.duration;
|
||||
if end_time <= self.current_time {
|
||||
return;
|
||||
}
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
if slowdown <= f64::EPSILON {
|
||||
// Zero slowdown will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't change current_time (since the incoming time values are always real-time), so
|
||||
// apply the slowdown by shifting the start time to compensate.
|
||||
if self.current_time <= time {
|
||||
let delta = time - self.current_time;
|
||||
|
||||
let max_delta = end_time - self.current_time;
|
||||
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Our slowdown value will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time -= adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time += delta - adjusted_delta;
|
||||
}
|
||||
} else {
|
||||
let delta = self.current_time - time;
|
||||
|
||||
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Current time was about to jump to before the animation had started; let's just
|
||||
// cancel the animation in this case.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time += adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time -= delta - adjusted_delta;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_time = time;
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.duration
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn is_clamped_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.clamped_duration
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.clamped_duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
@@ -292,7 +247,7 @@ impl Animation {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.current_time.saturating_sub(self.start_time);
|
||||
let passed = self.clock.now().saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Headless backend for tests.
|
||||
//!
|
||||
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
|
||||
//! crucial parts like rendering.
|
||||
|
||||
use std::mem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use niri_config::OutputName;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::element::RenderElementStates;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::utils::Size;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
|
||||
pub struct Headless {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
impl Headless {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ipc_outputs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, _niri: &mut Niri) {}
|
||||
|
||||
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
|
||||
let connector = format!("headless-{n}");
|
||||
let make = "niri".to_string();
|
||||
let model = "headless".to_string();
|
||||
let serial = n.to_string();
|
||||
|
||||
let output = Output::new(
|
||||
connector.clone(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: make.clone(),
|
||||
model: model.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let mode = Mode {
|
||||
size: Size::from((i32::from(size.0), i32::from(size.1))),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector,
|
||||
make: Some(make),
|
||||
model: Some(model),
|
||||
serial: Some(serial),
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
self.ipc_outputs.lock().unwrap().insert(
|
||||
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: size.0,
|
||||
height: size.1,
|
||||
refresh_rate: 60_000,
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
},
|
||||
);
|
||||
|
||||
niri.add_output(output, None, false);
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"headless".to_owned()
|
||||
}
|
||||
|
||||
pub fn with_primary_renderer<T>(
|
||||
&mut self,
|
||||
_f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
|
||||
let states = RenderElementStates::default();
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
Refresh::Unknown,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
RedrawState::Queued => (),
|
||||
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||
}
|
||||
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
// FIXME: request redraw on unfinished animations remain
|
||||
|
||||
RenderResult::Submitted
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||
self.ipc_outputs.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Headless {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+27
-7
@@ -17,9 +17,13 @@ pub use tty::Tty;
|
||||
pub mod winit;
|
||||
pub use winit::Winit;
|
||||
|
||||
pub mod headless;
|
||||
pub use headless::Headless;
|
||||
|
||||
pub enum Backend {
|
||||
Tty(Tty),
|
||||
Winit(Winit),
|
||||
Headless(Headless),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -54,6 +58,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.init(niri),
|
||||
Backend::Winit(winit) => winit.init(niri),
|
||||
Backend::Headless(headless) => headless.init(niri),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +66,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.seat_name(),
|
||||
Backend::Winit(winit) => winit.seat_name(),
|
||||
Backend::Headless(headless) => headless.seat_name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +77,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
||||
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
||||
Backend::Headless(headless) => headless.with_primary_renderer(f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +90,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
||||
Backend::Winit(winit) => winit.render(niri, output),
|
||||
Backend::Headless(headless) => headless.render(niri, output),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +98,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => CompositorMod::Alt,
|
||||
Backend::Headless(_) => CompositorMod::Super,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +106,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.change_vt(vt),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +114,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.suspend(),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +122,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
||||
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +130,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +138,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.early_import(surface),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +146,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||
Backend::Headless(headless) => headless.ipc_outputs(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +158,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||
Backend::Winit(_) => None,
|
||||
Backend::Headless(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +166,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +174,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +182,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||
Backend::Winit(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_debug_config_changed(&mut self) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_debug_config_changed(),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,4 +209,12 @@ impl Backend {
|
||||
panic!("backend is not Winit")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headless(&mut self) -> &mut Headless {
|
||||
if let Self::Headless(v) = self {
|
||||
v
|
||||
} else {
|
||||
panic!("backend is not Headless")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+306
-260
@@ -18,9 +18,9 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
|
||||
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
||||
use smithay::backend::drm::{
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
|
||||
};
|
||||
use smithay::backend::egl::context::ContextPriority;
|
||||
use smithay::backend::egl::{EGLDevice, EGLDisplay};
|
||||
@@ -50,6 +50,7 @@ use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlob
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
||||
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
||||
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
@@ -61,9 +62,14 @@ use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
|
||||
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
|
||||
Fourcc::Xrgb8888,
|
||||
Fourcc::Xbgr8888,
|
||||
Fourcc::Argb8888,
|
||||
Fourcc::Abgr8888,
|
||||
];
|
||||
|
||||
pub struct Tty {
|
||||
config: Rc<RefCell<Config>>,
|
||||
@@ -117,7 +123,7 @@ pub struct OutputDevice {
|
||||
render_node: DrmNode,
|
||||
drm_scanner: DrmScanner,
|
||||
surfaces: HashMap<crtc::Handle, Surface>,
|
||||
output_ids: HashMap<crtc::Handle, OutputId>,
|
||||
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
|
||||
// SAFETY: drop after all the objects used with them are dropped.
|
||||
// See https://github.com/Smithay/smithay/issues/1102.
|
||||
drm: DrmDevice,
|
||||
@@ -128,6 +134,13 @@ pub struct OutputDevice {
|
||||
active_leases: Vec<DrmLease>,
|
||||
}
|
||||
|
||||
// A connected, but not necessarily enabled, crtc.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrtcInfo {
|
||||
id: OutputId,
|
||||
name: OutputName,
|
||||
}
|
||||
|
||||
impl OutputDevice {
|
||||
pub fn lease_request(
|
||||
&self,
|
||||
@@ -167,6 +180,35 @@ impl OutputDevice {
|
||||
pub fn remove_lease(&mut self, lease_id: u32) {
|
||||
self.active_leases.retain(|l| l.id() != lease_id);
|
||||
}
|
||||
|
||||
pub fn known_crtc_name(
|
||||
&self,
|
||||
crtc: &crtc::Handle,
|
||||
conn: &connector::Info,
|
||||
disable_monitor_names: bool,
|
||||
) -> OutputName {
|
||||
if disable_monitor_names {
|
||||
let conn_name = format_connector_name(conn);
|
||||
return OutputName {
|
||||
connector: conn_name,
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
};
|
||||
}
|
||||
|
||||
let Some(info) = self.known_crtcs.get(crtc) else {
|
||||
let conn_name = format_connector_name(conn);
|
||||
error!("crtc for connector {conn_name} missing from known");
|
||||
return OutputName {
|
||||
connector: conn_name,
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
};
|
||||
};
|
||||
info.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -183,7 +225,6 @@ struct Surface {
|
||||
gamma_props: Option<GammaProps>,
|
||||
/// Gamma change to apply upon session resume.
|
||||
pending_gamma_change: Option<Option<Vec<u16>>>,
|
||||
vrr_enabled: bool,
|
||||
/// Tracy frame that goes from vblank to vblank.
|
||||
vblank_frame: Option<tracy_client::Frame>,
|
||||
/// Frame name for the VBlank frame.
|
||||
@@ -404,8 +445,6 @@ impl Tty {
|
||||
self.device_changed(node.dev_id(), niri);
|
||||
|
||||
// Apply pending gamma changes and restore our existing gamma.
|
||||
//
|
||||
// Also, restore our VRR.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
if let Some(ramp) = surface.pending_gamma_change.take() {
|
||||
@@ -423,33 +462,6 @@ impl Tty {
|
||||
warn!("error restoring gamma: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// Restore VRR.
|
||||
let output = niri
|
||||
.global_space
|
||||
.outputs()
|
||||
.find(|output| {
|
||||
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
||||
tty_state.node == node && tty_state.crtc == *crtc
|
||||
})
|
||||
.cloned();
|
||||
let Some(output) = output else {
|
||||
error!("missing output for crtc: {crtc:?}");
|
||||
continue;
|
||||
};
|
||||
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
||||
error!("missing state for output {:?}", surface.name.connector);
|
||||
continue;
|
||||
};
|
||||
|
||||
try_to_change_vrr(
|
||||
&device.drm,
|
||||
surface.connector,
|
||||
*crtc,
|
||||
surface,
|
||||
output_state,
|
||||
surface.vrr_enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +608,7 @@ impl Tty {
|
||||
gbm,
|
||||
drm_scanner: DrmScanner::new(),
|
||||
surfaces: HashMap::new(),
|
||||
output_ids: HashMap::new(),
|
||||
known_crtcs: HashMap::new(),
|
||||
drm_lease_state,
|
||||
active_leases: Vec::new(),
|
||||
non_desktop_connectors: HashSet::new(),
|
||||
@@ -629,6 +641,7 @@ impl Tty {
|
||||
}
|
||||
};
|
||||
|
||||
let mut added = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for event in scan_result {
|
||||
match event {
|
||||
@@ -636,34 +649,79 @@ impl Tty {
|
||||
connector,
|
||||
crtc: Some(crtc),
|
||||
} => {
|
||||
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
let connector_name = format_connector_name(&connector);
|
||||
let name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
debug!(
|
||||
"new connector: {} \"{}\"",
|
||||
&name.connector,
|
||||
name.format_make_model_serial(),
|
||||
);
|
||||
|
||||
// Assign an id to this crtc.
|
||||
let id = OutputId::next();
|
||||
added.push((crtc, CrtcInfo { id, name }));
|
||||
}
|
||||
DrmScanEvent::Disconnected {
|
||||
crtc: Some(crtc), ..
|
||||
} => {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
removed.push(crtc);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this is better done in connector_disconnected(), but currently we call that to
|
||||
// turn off outputs temporarily, too. So we can't do this there.
|
||||
for crtc in &removed {
|
||||
self.connector_disconnected(niri, node, *crtc);
|
||||
}
|
||||
|
||||
let Some(device) = self.devices.get_mut(&node) else {
|
||||
error!("device disappeared");
|
||||
return;
|
||||
};
|
||||
|
||||
for crtc in removed {
|
||||
if device.output_ids.remove(&crtc).is_none() {
|
||||
if device.known_crtcs.remove(&crtc).is_none() {
|
||||
error!("output ID missing for disconnected crtc: {crtc:?}");
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_ipc_outputs(niri);
|
||||
for (crtc, mut info) in added {
|
||||
// Make/model/serial can match exactly between different physical monitors. This doesn't
|
||||
// happen often, but our Layout does not support such duplicates and will panic.
|
||||
//
|
||||
// As a workaround, search for duplicates, and unname the new connectors if one is
|
||||
// found. Connector names are always unique.
|
||||
let name = &mut info.name;
|
||||
let formatted = name.format_make_model_serial_or_connector();
|
||||
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
|
||||
if info.name.matches(&formatted) {
|
||||
let connector = mem::take(&mut name.connector);
|
||||
warn!(
|
||||
"new connector {connector} duplicates make/model/serial \
|
||||
of existing connector {}, unnaming",
|
||||
info.name.connector,
|
||||
);
|
||||
*name = OutputName {
|
||||
connector,
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert it right away so next added connector will check against this one too.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
device.known_crtcs.insert(crtc, info);
|
||||
}
|
||||
|
||||
// This will connect any new connectors if needed, and apply other changes, such as
|
||||
// connecting back the internal laptop monitor once it becomes the only monitor left.
|
||||
//
|
||||
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
|
||||
// above.
|
||||
self.on_output_config_changed(niri);
|
||||
}
|
||||
|
||||
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
|
||||
@@ -749,7 +807,8 @@ impl Tty {
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
|
||||
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
|
||||
|
||||
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
|
||||
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
|
||||
@@ -767,10 +826,6 @@ impl Tty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// This should be unique per CRTC connection, however currently we can call
|
||||
// connector_connected() multiple times for turning the output off and on.
|
||||
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
|
||||
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -779,11 +834,6 @@ impl Tty {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if config.off {
|
||||
debug!("output is disabled in the config");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for m in connector.modes() {
|
||||
trace!("{m:?}");
|
||||
}
|
||||
@@ -811,45 +861,6 @@ impl Tty {
|
||||
Err(err) => debug!("error setting max bpc: {err:?}"),
|
||||
}
|
||||
|
||||
// Try to enable VRR if requested.
|
||||
let mut vrr_enabled = false;
|
||||
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
|
||||
if capable {
|
||||
// Even if on-demand, we still disable it until later checks.
|
||||
let vrr = config.is_vrr_always_on();
|
||||
let word = if vrr { "enabling" } else { "disabling" };
|
||||
|
||||
match set_vrr_enabled(&device.drm, crtc, vrr) {
|
||||
Ok(enabled) => {
|
||||
if enabled != vrr {
|
||||
warn!("failed {} VRR", word);
|
||||
}
|
||||
|
||||
vrr_enabled = enabled;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error {} VRR: {err:?}", word);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !config.is_vrr_always_off() {
|
||||
warn!("cannot enable VRR because connector is not vrr_capable");
|
||||
}
|
||||
|
||||
// Try to disable it anyway to work around a bug where resetting DRM state causes
|
||||
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
|
||||
let res = set_vrr_enabled(&device.drm, crtc, false);
|
||||
if matches!(res, Ok(true)) {
|
||||
warn!("error disabling VRR");
|
||||
|
||||
// So that we can try it again later.
|
||||
vrr_enabled = true;
|
||||
}
|
||||
}
|
||||
} else if !config.is_vrr_always_off() {
|
||||
warn!("cannot enable VRR because connector is not vrr_capable");
|
||||
}
|
||||
|
||||
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
||||
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
|
||||
.ok();
|
||||
@@ -868,6 +879,31 @@ impl Tty {
|
||||
.drm
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
|
||||
// Try to enable VRR if requested.
|
||||
match surface.vrr_supported(connector.handle()) {
|
||||
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
|
||||
// Even if on-demand, we still disable it until later checks.
|
||||
let vrr = config.is_vrr_always_on();
|
||||
let word = if vrr { "enabling" } else { "disabling" };
|
||||
|
||||
if let Err(err) = surface.use_vrr(vrr) {
|
||||
warn!("error {} VRR: {err:?}", word);
|
||||
}
|
||||
}
|
||||
Ok(VrrSupport::NotSupported) => {
|
||||
if !config.is_vrr_always_off() {
|
||||
warn!("cannot enable VRR because connector does not support it");
|
||||
}
|
||||
|
||||
// Try to disable it anyway to work around a bug where resetting DRM state causes
|
||||
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
|
||||
let _ = surface.use_vrr(false);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error querying for VRR support: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// Create GBM allocator.
|
||||
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
||||
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
|
||||
@@ -894,23 +930,6 @@ impl Tty {
|
||||
.insert_if_missing(|| TtyOutputState { node, crtc });
|
||||
output.user_data().insert_if_missing(|| output_name.clone());
|
||||
|
||||
let mut planes = surface.planes().clone();
|
||||
|
||||
let config = self.config.borrow();
|
||||
|
||||
// Overlay planes are disabled by default as they cause weird performance issues on my
|
||||
// system.
|
||||
if !config.debug.enable_overlay_planes {
|
||||
planes.overlay.clear();
|
||||
}
|
||||
|
||||
// Cursor planes have bugs on some systems.
|
||||
let cursor_plane_gbm = if config.debug.disable_cursor_plane {
|
||||
None
|
||||
} else {
|
||||
Some(device.gbm.clone())
|
||||
};
|
||||
|
||||
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
|
||||
let egl_context = renderer.as_ref().egl_context();
|
||||
let render_formats = egl_context.dmabuf_render_formats();
|
||||
@@ -949,7 +968,7 @@ impl Tty {
|
||||
let res = DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
surface,
|
||||
Some(planes),
|
||||
None,
|
||||
allocator.clone(),
|
||||
device.gbm.clone(),
|
||||
SUPPORTED_COLOR_FORMATS,
|
||||
@@ -957,7 +976,7 @@ impl Tty {
|
||||
// formats, even though we only ever render on the primary GPU.
|
||||
render_formats.clone(),
|
||||
device.drm.cursor_size(),
|
||||
cursor_plane_gbm.clone(),
|
||||
Some(device.gbm.clone()),
|
||||
);
|
||||
|
||||
let mut compositor = match res {
|
||||
@@ -975,21 +994,17 @@ impl Tty {
|
||||
let surface = device
|
||||
.drm
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
let mut planes = surface.planes().clone();
|
||||
if !config.debug.enable_overlay_planes {
|
||||
planes.overlay.clear();
|
||||
}
|
||||
|
||||
DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
surface,
|
||||
Some(planes),
|
||||
None,
|
||||
allocator,
|
||||
device.gbm.clone(),
|
||||
SUPPORTED_COLOR_FORMATS,
|
||||
render_formats,
|
||||
device.drm.cursor_size(),
|
||||
cursor_plane_gbm,
|
||||
Some(device.gbm.clone()),
|
||||
)
|
||||
.context("error creating DRM compositor")?
|
||||
}
|
||||
@@ -998,7 +1013,6 @@ impl Tty {
|
||||
if self.debug_tint {
|
||||
compositor.set_debug_flags(DebugFlags::TINT);
|
||||
}
|
||||
compositor.use_direct_scanout(!config.debug.disable_direct_scanout);
|
||||
|
||||
let mut dmabuf_feedback = None;
|
||||
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
|
||||
@@ -1019,6 +1033,16 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
// Some buggy monitors replug upon powering off, so powering on here would prevent such
|
||||
// monitors from powering off. Therefore, we avoid unconditionally powering on.
|
||||
if !niri.monitors_active {
|
||||
if let Err(err) = compositor.clear() {
|
||||
warn!("error clearing drm surface: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let vrr_enabled = compositor.vrr_enabled();
|
||||
|
||||
let vblank_frame_name =
|
||||
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
|
||||
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
|
||||
@@ -1036,7 +1060,6 @@ impl Tty {
|
||||
compositor,
|
||||
dmabuf_feedback,
|
||||
gamma_props,
|
||||
vrr_enabled,
|
||||
pending_gamma_change: None,
|
||||
vblank_frame: None,
|
||||
vblank_frame_name,
|
||||
@@ -1050,15 +1073,14 @@ impl Tty {
|
||||
|
||||
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
|
||||
|
||||
// Some buggy monitors replug upon powering off, so powering on here would prevent such
|
||||
// monitors from powering off. Therefore, we avoid unconditionally powering on.
|
||||
if niri.monitors_active {
|
||||
// Redraw the new monitor.
|
||||
niri.event_loop.insert_idle(move |state| {
|
||||
state.niri.queue_redraw(&output);
|
||||
// Guard against output disconnecting before the idle has a chance to run.
|
||||
if state.niri.output_state.contains_key(&output) {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
set_crtc_active(&device.drm, crtc, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1216,10 +1238,17 @@ impl Tty {
|
||||
// Mark the last frame as submitted.
|
||||
match surface.compositor.frame_submitted() {
|
||||
Ok(Some((mut feedback, target_presentation_time))) => {
|
||||
let refresh = output_state
|
||||
.frame_clock
|
||||
.refresh_interval()
|
||||
.unwrap_or(Duration::ZERO);
|
||||
let refresh = match output_state.frame_clock.refresh_interval() {
|
||||
Some(refresh) => {
|
||||
if output_state.frame_clock.vrr() {
|
||||
Refresh::Variable(refresh)
|
||||
} else {
|
||||
Refresh::Fixed(refresh)
|
||||
}
|
||||
}
|
||||
None => Refresh::Unknown,
|
||||
};
|
||||
|
||||
// FIXME: ideally should be monotonically increasing for a surface.
|
||||
let seq = meta.sequence as u64;
|
||||
let mut flags = wp_presentation_feedback::Kind::Vsync
|
||||
@@ -1369,9 +1398,35 @@ impl Tty {
|
||||
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
||||
}
|
||||
|
||||
// Overlay planes are disabled by default as they cause weird performance issues on my
|
||||
// system.
|
||||
let flags = {
|
||||
let debug = &self.config.borrow().debug;
|
||||
|
||||
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
|
||||
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
|
||||
} else {
|
||||
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
|
||||
};
|
||||
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
|
||||
|
||||
if debug.enable_overlay_planes {
|
||||
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
||||
}
|
||||
if debug.disable_direct_scanout {
|
||||
flags.remove(primary_scanout_flag);
|
||||
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
||||
}
|
||||
if debug.disable_cursor_plane {
|
||||
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
|
||||
}
|
||||
|
||||
flags
|
||||
};
|
||||
|
||||
// Hand them over to the DRM.
|
||||
let drm_compositor = &mut surface.compositor;
|
||||
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
|
||||
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
|
||||
Ok(res) => {
|
||||
let needs_sync = res.needs_sync()
|
||||
|| self
|
||||
@@ -1552,13 +1607,13 @@ impl Tty {
|
||||
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
|
||||
|
||||
let mut ipc_outputs = HashMap::new();
|
||||
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||
|
||||
for (node, device) in &self.devices {
|
||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||
let connector_name = format_connector_name(connector);
|
||||
let physical_size = connector.size();
|
||||
let output_name =
|
||||
make_output_name(&device.drm, connector.handle(), connector_name.clone());
|
||||
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||
|
||||
let surface = device.surfaces.get(&crtc);
|
||||
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
||||
@@ -1593,8 +1648,17 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
let vrr_supported = is_vrr_capable(&device.drm, connector.handle()) == Some(true);
|
||||
let vrr_enabled = surface.map_or(false, |surface| surface.vrr_enabled);
|
||||
let vrr_supported = surface
|
||||
.map(|surface| {
|
||||
matches!(
|
||||
surface.compositor.vrr_supported(connector.handle()),
|
||||
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
|
||||
});
|
||||
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
|
||||
|
||||
let logical = niri
|
||||
.global_space
|
||||
@@ -1605,6 +1669,12 @@ impl Tty {
|
||||
})
|
||||
.map(logical_output);
|
||||
|
||||
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
|
||||
let id = id.unwrap_or_else(|| {
|
||||
error!("crtc for connector {connector_name} missing from known");
|
||||
OutputId::next()
|
||||
});
|
||||
|
||||
let ipc_output = niri_ipc::Output {
|
||||
name: connector_name,
|
||||
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
||||
@@ -1618,10 +1688,6 @@ impl Tty {
|
||||
logical,
|
||||
};
|
||||
|
||||
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
|
||||
error!("output ID missing for crtc: {crtc:?}");
|
||||
OutputId::next()
|
||||
});
|
||||
ipc_outputs.insert(id, ipc_output);
|
||||
}
|
||||
}
|
||||
@@ -1659,10 +1725,9 @@ impl Tty {
|
||||
}
|
||||
|
||||
for device in self.devices.values_mut() {
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
set_crtc_active(&device.drm, *crtc, false);
|
||||
if let Err(err) = surface.compositor.reset_state() {
|
||||
warn!("error resetting surface state: {err:?}");
|
||||
for surface in device.surfaces.values_mut() {
|
||||
if let Err(err) = surface.compositor.clear() {
|
||||
warn!("error clearing drm surface: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1680,14 +1745,17 @@ impl Tty {
|
||||
for (&crtc, surface) in device.surfaces.iter_mut() {
|
||||
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
||||
if tty_state.node == node && tty_state.crtc == crtc {
|
||||
try_to_change_vrr(
|
||||
&device.drm,
|
||||
surface.connector,
|
||||
crtc,
|
||||
surface,
|
||||
output_state,
|
||||
enable_vrr,
|
||||
);
|
||||
let word = if enable_vrr { "enabling" } else { "disabling" };
|
||||
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
|
||||
warn!(
|
||||
"output {:?}: error {} VRR: {err:?}",
|
||||
surface.name.connector, word
|
||||
);
|
||||
}
|
||||
output_state
|
||||
.frame_clock
|
||||
.set_vrr(surface.compositor.vrr_enabled());
|
||||
|
||||
self.refresh_ipc_outputs(niri);
|
||||
return;
|
||||
}
|
||||
@@ -1705,6 +1773,24 @@ impl Tty {
|
||||
}
|
||||
self.update_output_config_on_resume = false;
|
||||
|
||||
// Figure out if we should disable laptop panels.
|
||||
let mut disable_laptop_panels = false;
|
||||
if niri.is_lid_closed {
|
||||
let config = self.config.borrow();
|
||||
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
|
||||
// Check if any external monitor is connected.
|
||||
'outer: for device in self.devices.values() {
|
||||
for (connector, _crtc) in device.drm_scanner.crtcs() {
|
||||
if !is_laptop_panel(&format_connector_name(connector)) {
|
||||
disable_laptop_panels = true;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
|
||||
|
||||
let mut to_disconnect = vec![];
|
||||
let mut to_connect = vec![];
|
||||
|
||||
@@ -1717,7 +1803,7 @@ impl Tty {
|
||||
.find(&surface.name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if config.off {
|
||||
if config.off || should_disable(&surface.name.connector) {
|
||||
to_disconnect.push((node, crtc));
|
||||
continue;
|
||||
}
|
||||
@@ -1735,8 +1821,11 @@ impl Tty {
|
||||
};
|
||||
|
||||
let change_mode = surface.compositor.pending_mode() != mode;
|
||||
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
|
||||
|
||||
let vrr_enabled = surface.compositor.vrr_enabled();
|
||||
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
|
||||
let is_on_demand_vrr = config.is_vrr_on_demand();
|
||||
|
||||
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
|
||||
continue;
|
||||
}
|
||||
@@ -1758,17 +1847,20 @@ impl Tty {
|
||||
continue;
|
||||
};
|
||||
|
||||
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
|
||||
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
|
||||
|| (!is_on_demand_vrr && change_always_vrr)
|
||||
{
|
||||
try_to_change_vrr(
|
||||
&device.drm,
|
||||
connector.handle(),
|
||||
crtc,
|
||||
surface,
|
||||
output_state,
|
||||
!surface.vrr_enabled,
|
||||
);
|
||||
let vrr = !vrr_enabled;
|
||||
let word = if vrr { "enabling" } else { "disabling" };
|
||||
if let Err(err) = surface.compositor.use_vrr(vrr) {
|
||||
warn!(
|
||||
"output {:?}: error {} VRR: {err:?}",
|
||||
surface.name.connector, word
|
||||
);
|
||||
}
|
||||
output_state
|
||||
.frame_clock
|
||||
.set_vrr(surface.compositor.vrr_enabled());
|
||||
}
|
||||
|
||||
if change_mode {
|
||||
@@ -1800,12 +1892,17 @@ impl Tty {
|
||||
let wl_mode = Mode::from(mode);
|
||||
output.change_current_state(Some(wl_mode), None, None, None);
|
||||
output.set_preferred(wl_mode);
|
||||
output_state.frame_clock =
|
||||
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
|
||||
output_state.frame_clock = FrameClock::new(
|
||||
Some(refresh_interval(mode)),
|
||||
surface.compositor.vrr_enabled(),
|
||||
);
|
||||
niri.output_resized(&output);
|
||||
}
|
||||
}
|
||||
|
||||
let config = self.config.borrow();
|
||||
let disable_monitor_names = config.debug.disable_monitor_names;
|
||||
|
||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||
// Check if connected.
|
||||
if connector.state() != connector::State::Connected {
|
||||
@@ -1813,22 +1910,24 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if already enabled.
|
||||
if device.surfaces.contains_key(&crtc) {
|
||||
if device.surfaces.contains_key(&crtc)
|
||||
|| device
|
||||
.non_desktop_connectors
|
||||
.contains(&(connector.handle(), crtc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let connector_name = format_connector_name(connector);
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||
|
||||
let config = config
|
||||
.outputs
|
||||
.find(&output_name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if !config.off {
|
||||
to_connect.push((node, connector.clone(), crtc));
|
||||
if !(config.off || should_disable(&output_name.connector)) {
|
||||
to_connect.push((node, connector.clone(), crtc, output_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1837,7 +1936,11 @@ impl Tty {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
}
|
||||
|
||||
for (node, connector, crtc) in to_connect {
|
||||
// Sort by output name to get more predictable first focused output at initial compositor
|
||||
// startup, when multiple connectors appear at once.
|
||||
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
|
||||
|
||||
for (node, connector, crtc, _name) in to_connect {
|
||||
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
@@ -1846,24 +1949,12 @@ impl Tty {
|
||||
self.refresh_ipc_outputs(niri);
|
||||
}
|
||||
|
||||
pub fn on_debug_config_changed(&mut self) {
|
||||
let config = self.config.borrow();
|
||||
let debug = &config.debug;
|
||||
let use_direct_scanout = !debug.disable_direct_scanout;
|
||||
|
||||
// FIXME: reload other flags if possible?
|
||||
for device in self.devices.values_mut() {
|
||||
for surface in device.surfaces.values_mut() {
|
||||
surface.compositor.use_direct_scanout(use_direct_scanout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
||||
self.devices.get_mut(&node)
|
||||
}
|
||||
|
||||
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
|
||||
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||
for device in self.devices.values() {
|
||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||
// Check if connected.
|
||||
@@ -1872,12 +1963,15 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if already enabled.
|
||||
if device.surfaces.contains_key(&crtc) {
|
||||
if device.surfaces.contains_key(&crtc)
|
||||
|| device
|
||||
.non_desktop_connectors
|
||||
.contains(&(connector.handle(), crtc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let connector_name = format_connector_name(connector);
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||
if output_name.matches(target) {
|
||||
return Some(output_name);
|
||||
}
|
||||
@@ -2072,9 +2166,8 @@ fn surface_dmabuf_feedback(
|
||||
let surface = compositor.surface();
|
||||
let planes = surface.planes();
|
||||
|
||||
let plane_formats = surface
|
||||
.plane_info()
|
||||
.formats
|
||||
let primary_plane_formats = surface.plane_info().formats.clone();
|
||||
let primary_or_overlay_plane_formats = primary_plane_formats
|
||||
.iter()
|
||||
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
|
||||
.copied()
|
||||
@@ -2082,7 +2175,11 @@ fn surface_dmabuf_feedback(
|
||||
|
||||
// We limit the scan-out trache to formats we can also render from so that there is always a
|
||||
// fallback render path available in case the supplied buffer can not be scanned out directly.
|
||||
let mut scanout_formats = plane_formats
|
||||
let mut primary_scanout_formats = primary_plane_formats
|
||||
.intersection(&primary_formats)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
|
||||
.intersection(&primary_formats)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -2090,17 +2187,32 @@ fn surface_dmabuf_feedback(
|
||||
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
|
||||
// buffers produce a glitched scanout if the modifier is not Linear...
|
||||
if primary_render_node != surface_render_node {
|
||||
scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
||||
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
||||
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
||||
}
|
||||
|
||||
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
|
||||
|
||||
trace!(
|
||||
"primary scanout formats: {}, overlay adds: {}",
|
||||
primary_scanout_formats.len(),
|
||||
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
|
||||
);
|
||||
|
||||
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
|
||||
// increase the chance of scanning out a client even with our disabled-by-default overlay
|
||||
// planes.
|
||||
let scanout = builder
|
||||
.clone()
|
||||
.add_preference_tranche(
|
||||
surface_render_node.dev_id(),
|
||||
Some(TrancheFlags::Scanout),
|
||||
scanout_formats,
|
||||
primary_scanout_formats,
|
||||
)
|
||||
.add_preference_tranche(
|
||||
surface_render_node.dev_id(),
|
||||
Some(TrancheFlags::Scanout),
|
||||
primary_or_overlay_scanout_formats,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
@@ -2154,17 +2266,6 @@ fn get_drm_property(
|
||||
.find_map(|(handle, value)| (handle == prop).then_some(value))
|
||||
}
|
||||
|
||||
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
|
||||
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let value = property::Value::Boolean(active);
|
||||
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
|
||||
warn!("error setting CRTC property: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_interval(mode: DrmMode) -> Duration {
|
||||
let clock = mode.clock() as u64;
|
||||
let htotal = mode.hsync().2 as u64;
|
||||
@@ -2374,24 +2475,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
|
||||
info.value_type().convert_value(value).as_boolean()
|
||||
}
|
||||
|
||||
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
|
||||
let (prop, info, _) =
|
||||
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
|
||||
|
||||
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
|
||||
device
|
||||
.set_property(crtc, prop, value.into())
|
||||
.context("error setting VRR_ENABLED property")?;
|
||||
|
||||
let value = get_drm_property(device, crtc, prop)
|
||||
.context("VRR_ENABLED property missing after setting")?;
|
||||
match info.value_type().convert_value(value) {
|
||||
property::Value::UnsignedRange(value) => Ok(value == 1),
|
||||
property::Value::Boolean(value) => Ok(value),
|
||||
_ => bail!("wrong VRR_ENABLED property type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_gamma_for_crtc(
|
||||
device: &DrmDevice,
|
||||
crtc: crtc::Handle,
|
||||
@@ -2437,43 +2520,6 @@ pub fn set_gamma_for_crtc(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_to_change_vrr(
|
||||
device: &DrmDevice,
|
||||
connector: connector::Handle,
|
||||
crtc: crtc::Handle,
|
||||
surface: &mut Surface,
|
||||
output_state: &mut crate::niri::OutputState,
|
||||
enable_vrr: bool,
|
||||
) {
|
||||
let _span = tracy_client::span!("try_to_change_vrr");
|
||||
|
||||
if is_vrr_capable(device, connector) == Some(true) {
|
||||
let word = if enable_vrr { "enabling" } else { "disabling" };
|
||||
|
||||
match set_vrr_enabled(device, crtc, enable_vrr) {
|
||||
Ok(enabled) => {
|
||||
if enabled != enable_vrr {
|
||||
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
|
||||
}
|
||||
|
||||
surface.vrr_enabled = enabled;
|
||||
output_state.frame_clock.set_vrr(enabled);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"output {:?}: error {} VRR: {err:?}",
|
||||
surface.name.connector, word
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if enable_vrr {
|
||||
warn!(
|
||||
"output {:?}: cannot enable VRR because connector is not vrr_capable",
|
||||
surface.name.connector
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn format_connector_name(connector: &connector::Info) -> String {
|
||||
format!(
|
||||
"{}-{}",
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
@@ -16,6 +15,7 @@ use smithay::reexports::calloop::LoopHandle;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
@@ -216,11 +216,9 @@ impl Winit {
|
||||
self.backend.submit(Some(damage)).unwrap();
|
||||
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
||||
let mode = output.current_mode().unwrap();
|
||||
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
refresh,
|
||||
Refresh::Unknown,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
|
||||
@@ -64,6 +64,8 @@ pub enum Msg {
|
||||
Workspaces,
|
||||
/// List open windows.
|
||||
Windows,
|
||||
/// List open layer-shell surfaces.
|
||||
Layers,
|
||||
/// Get the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Print information about the focused output.
|
||||
|
||||
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use anyhow::Context;
|
||||
use futures_util::StreamExt;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::message::Header;
|
||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||
use zbus::zvariant::NoneValue;
|
||||
use zbus::{dbus_interface, MessageHeader, Task};
|
||||
use zbus::{interface, Task};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -20,11 +21,11 @@ pub struct ScreenSaver {
|
||||
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||
#[interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
async fn inhibit(
|
||||
&mut self,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
#[zbus(header)] hdr: Header<'_>,
|
||||
application_name: &str,
|
||||
reason_for_inhibit: &str,
|
||||
) -> fdo::Result<u32> {
|
||||
@@ -33,7 +34,7 @@ impl ScreenSaver {
|
||||
hdr.sender()
|
||||
);
|
||||
|
||||
let Ok(Some(name)) = hdr.sender() else {
|
||||
let Some(name) = hdr.sender() else {
|
||||
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||
};
|
||||
let name = OwnedUniqueName::from(name.to_owned());
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{SerializeDict, Type, Value};
|
||||
use zbus::{dbus_interface, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -33,7 +34,7 @@ pub struct WindowProperties {
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
|
||||
#[interface(name = "org.gnome.Shell.Introspect")]
|
||||
impl Introspect {
|
||||
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
|
||||
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
|
||||
@@ -52,8 +53,8 @@ impl Introspect {
|
||||
|
||||
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
|
||||
// needed for the event stream IPC anyway).
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl Introspect {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use zbus::dbus_interface;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -18,7 +18,7 @@ pub enum NiriToScreenshot {
|
||||
ScreenshotResult(Option<PathBuf>),
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
|
||||
#[interface(name = "org.gnome.Shell.Screenshot")]
|
||||
impl Screenshot {
|
||||
async fn screenshot(
|
||||
&self,
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::Interface;
|
||||
use zbus::object_server::Interface;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
@@ -83,7 +83,7 @@ impl DBusServers {
|
||||
dbus.conn_introspect = try_start(introspect);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
if niri.pipewire.is_some() {
|
||||
{
|
||||
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
||||
niri.event_loop
|
||||
.insert_source(from_screen_cast, {
|
||||
@@ -95,8 +95,6 @@ impl DBusServers {
|
||||
.unwrap();
|
||||
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
||||
dbus.conn_screen_cast = try_start(screen_cast);
|
||||
} else {
|
||||
warn!("disabling screencast support because we couldn't start PipeWire");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
use zbus::{dbus_interface, fdo, SignalContext};
|
||||
use zbus::{fdo, interface};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -42,7 +44,7 @@ pub struct LogicalMonitor {
|
||||
properties: HashMap<String, OwnedValue>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
impl DisplayConfig {
|
||||
async fn get_current_state(
|
||||
&self,
|
||||
@@ -63,7 +65,7 @@ impl DisplayConfig {
|
||||
.map(|output| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
@@ -155,8 +157,8 @@ impl DisplayConfig {
|
||||
Ok((0, monitors, logical_monitors, properties))
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
@@ -211,16 +213,16 @@ fn format_diagonal(diagonal_inches: f64) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use k9::snapshot;
|
||||
use insta::assert_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″");
|
||||
assert_snapshot!(format_diagonal(12.11), @"12.1″");
|
||||
assert_snapshot!(format_diagonal(13.28), @"13.3″");
|
||||
assert_snapshot!(format_diagonal(15.6), @"15.6″");
|
||||
assert_snapshot!(format_diagonal(23.2), @"23″");
|
||||
assert_snapshot!(format_diagonal(24.8), @"25″");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::{InterfaceRef, SignalEmitter};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||
use zbus::{fdo, interface, ObjectServer};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
@@ -94,14 +95,14 @@ pub enum ScreenCastToNiri {
|
||||
session_id: usize,
|
||||
target: StreamTargetId,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalContext<'static>,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
},
|
||||
StopCast {
|
||||
session_id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
impl ScreenCast {
|
||||
async fn create_session(
|
||||
&self,
|
||||
@@ -136,26 +137,26 @@ impl ScreenCast {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
#[zbus(property)]
|
||||
async fn version(&self) -> i32 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
impl Session {
|
||||
async fn start(&self) {
|
||||
debug!("start");
|
||||
|
||||
for (stream, iface) in &*self.streams.lock().unwrap() {
|
||||
stream.start(self.id, iface.signal_context().clone());
|
||||
stream.start(self.id, iface.signal_emitter().clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(
|
||||
&self,
|
||||
#[zbus(object_server)] server: &ObjectServer,
|
||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
||||
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
|
||||
) {
|
||||
debug!("stop");
|
||||
|
||||
@@ -175,7 +176,7 @@ impl Session {
|
||||
let streams = mem::take(&mut *self.streams.lock().unwrap());
|
||||
for (_, iface) in streams.iter() {
|
||||
server
|
||||
.remove::<Stream, _>(iface.signal_context().path())
|
||||
.remove::<Stream, _>(iface.signal_emitter().path())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -264,17 +265,17 @@ impl Session {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
impl Stream {
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||
#[zbus(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(property)]
|
||||
#[zbus(property)]
|
||||
async fn parameters(&self) -> StreamParameters {
|
||||
match &self.target {
|
||||
StreamTarget::Output(output) => {
|
||||
@@ -361,7 +362,7 @@ impl Stream {
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
|
||||
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
|
||||
if self.was_started.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::reexports::wayland_server::DisplayHandle;
|
||||
use zbus::dbus_interface;
|
||||
use zbus::{fdo, interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
use crate::niri::ClientState;
|
||||
@@ -12,14 +11,14 @@ pub struct ServiceChannel {
|
||||
display: DisplayHandle,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
impl ServiceChannel {
|
||||
async fn open_wayland_service_connection(
|
||||
&mut self,
|
||||
service_client_type: u32,
|
||||
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
|
||||
) -> fdo::Result<zvariant::OwnedFd> {
|
||||
if service_client_type != 1 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
return Err(fdo::Error::InvalidArgs(
|
||||
"Invalid service client type".to_owned(),
|
||||
));
|
||||
}
|
||||
@@ -30,9 +29,11 @@ impl ServiceChannel {
|
||||
// Would be nice to thread config here but for now it's fine.
|
||||
can_view_decoration_globals: false,
|
||||
restricted: false,
|
||||
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||
credentials_unknown: true,
|
||||
});
|
||||
self.display.insert_client(sock2, data).unwrap();
|
||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
||||
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ impl ServiceChannel {
|
||||
|
||||
impl Start for ServiceChannel {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let conn = zbus::blocking::ConnectionBuilder::session()?
|
||||
let conn = zbus::blocking::connection::Builder::session()?
|
||||
.name("org.gnome.Mutter.ServiceChannel")?
|
||||
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
|
||||
.build()?;
|
||||
|
||||
+134
-32
@@ -1,7 +1,8 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use niri_ipc::PositionChange;
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::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;
|
||||
@@ -18,6 +19,8 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{delegate_compositor, delegate_shm};
|
||||
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::send_scale_transform;
|
||||
use crate::utils::transaction::Transaction;
|
||||
@@ -84,16 +87,23 @@ impl CompositorHandler for State {
|
||||
|
||||
if is_mapped {
|
||||
// The toplevel got mapped.
|
||||
let Unmapped { window, state } = entry.remove();
|
||||
let Unmapped {
|
||||
window,
|
||||
state,
|
||||
activation_token_data,
|
||||
} = entry.remove();
|
||||
|
||||
window.on_commit();
|
||||
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
|
||||
let (rules, width, is_full_width, output, workspace_name) =
|
||||
let (rules, width, height, is_full_width, output, workspace_id) =
|
||||
if let InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
height,
|
||||
floating_width: _,
|
||||
floating_height: _,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
@@ -104,15 +114,48 @@ impl CompositorHandler for State {
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
// Check that the workspace still exists.
|
||||
let workspace_name = workspace_name
|
||||
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
|
||||
let workspace_id = workspace_name
|
||||
.as_deref()
|
||||
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
|
||||
.map(|(_, ws)| ws.id());
|
||||
|
||||
(rules, width, is_full_width, output, workspace_name)
|
||||
(rules, width, height, is_full_width, output, workspace_id)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(ResolvedWindowRules::empty(), None, false, None, None)
|
||||
(ResolvedWindowRules::empty(), None, None, false, None, None)
|
||||
};
|
||||
|
||||
// The GTK about dialog sets min/max size after the initial configure but
|
||||
// before mapping, so we need to compute open_floating at the last possible
|
||||
// moment, that is here.
|
||||
let is_floating = rules.compute_open_floating(toplevel);
|
||||
|
||||
// Figure out if we should activate the window.
|
||||
let activate = rules.open_focused.map(|focus| {
|
||||
if focus {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
ActivateWindow::No
|
||||
}
|
||||
});
|
||||
let activate = activate.unwrap_or_else(|| {
|
||||
// Check the token timestamp again in case the window took a while between
|
||||
// requesting activation and mapping.
|
||||
let token = activation_token_data.filter(|token| {
|
||||
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
|
||||
});
|
||||
if token.is_some() {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.strict_new_window_focus_policy {
|
||||
ActivateWindow::No
|
||||
} else {
|
||||
ActivateWindow::Smart
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let parent = toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
@@ -133,34 +176,34 @@ impl CompositorHandler for State {
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let window = mapped.window.clone();
|
||||
|
||||
let output = if let Some(p) = parent {
|
||||
// Open dialogs immediately to the right of their parent window.
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_right_of(&p, mapped, width, is_full_width)
|
||||
} else if let Some(workspace_name) = &workspace_name {
|
||||
self.niri.layout.add_window_to_named_workspace(
|
||||
workspace_name,
|
||||
mapped,
|
||||
width,
|
||||
is_full_width,
|
||||
)
|
||||
let target = if let Some(p) = &parent {
|
||||
// Open dialogs next to their parent window.
|
||||
AddWindowTarget::NextTo(p)
|
||||
} else if let Some(id) = workspace_id {
|
||||
AddWindowTarget::Workspace(id)
|
||||
} else if let Some(output) = &output {
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_on_output(output, mapped, width, is_full_width);
|
||||
Some(output)
|
||||
AddWindowTarget::Output(output)
|
||||
} else {
|
||||
self.niri.layout.add_window(mapped, width, is_full_width)
|
||||
AddWindowTarget::Auto
|
||||
};
|
||||
let output = self.niri.layout.add_window(
|
||||
mapped,
|
||||
target,
|
||||
width,
|
||||
height,
|
||||
is_full_width,
|
||||
is_floating,
|
||||
activate,
|
||||
);
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
self.niri.layout.start_open_animation_for_window(&window);
|
||||
|
||||
let new_active_window =
|
||||
self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||
if new_active_window == Some(&window) {
|
||||
let new_focus = self.niri.layout.focus().map(|m| &m.window);
|
||||
if new_focus == Some(&window) {
|
||||
// We activated the newly opened window.
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
@@ -210,7 +253,7 @@ impl CompositorHandler for State {
|
||||
// The toplevel got unmapped.
|
||||
//
|
||||
// Test client: wleird-unmap.
|
||||
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
@@ -241,14 +284,21 @@ impl CompositorHandler for State {
|
||||
return;
|
||||
}
|
||||
|
||||
let serial = with_states(surface, |states| {
|
||||
let (serial, buffer_delta) = with_states(surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
role.configure_serial
|
||||
(role.configure_serial, buffer_delta)
|
||||
});
|
||||
if serial.is_none() {
|
||||
error!("commit on a mapped surface without a configured serial");
|
||||
@@ -257,8 +307,21 @@ impl CompositorHandler for State {
|
||||
// The toplevel remains mapped.
|
||||
self.niri.layout.update_window(&window, serial);
|
||||
|
||||
// Move the toplevel according to the attach offset.
|
||||
if let Some(delta) = buffer_delta {
|
||||
if delta.x != 0 || delta.y != 0 {
|
||||
let (x, y) = delta.to_f64().into();
|
||||
self.niri.layout.move_floating_window(
|
||||
Some(&window),
|
||||
PositionChange::AdjustFixed(x),
|
||||
PositionChange::AdjustFixed(y),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window, &output);
|
||||
self.update_reactive_popups(&window);
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
return;
|
||||
@@ -297,13 +360,52 @@ impl CompositorHandler for State {
|
||||
&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(&root_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;
|
||||
|
||||
@@ -11,6 +11,7 @@ use smithay::wayland::shell::wlr_layer::{
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||
use crate::niri::State;
|
||||
use crate::utils::send_scale_transform;
|
||||
|
||||
@@ -60,6 +61,7 @@ impl WlrLayerShellHandler for State {
|
||||
layer.map(|layer| (o.clone(), map, layer))
|
||||
}) {
|
||||
map.unmap_layer(&layer);
|
||||
self.niri.mapped_layer_surfaces.remove(&layer);
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
@@ -128,6 +130,21 @@ impl State {
|
||||
if is_mapped {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let rules = &self.niri.config.borrow().layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules);
|
||||
let prev = self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
.insert(layer.clone(), mapped);
|
||||
if prev.is_some() {
|
||||
error!("MappedLayer was present for an unmapped 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
|
||||
@@ -151,6 +168,7 @@ impl State {
|
||||
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
||||
}
|
||||
} else {
|
||||
self.niri.mapped_layer_surfaces.remove(layer);
|
||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||
}
|
||||
} else {
|
||||
|
||||
+144
-24
@@ -7,12 +7,15 @@ 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};
|
||||
@@ -22,8 +25,8 @@ 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,
|
||||
@@ -33,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,
|
||||
};
|
||||
@@ -59,12 +62,12 @@ use smithay::{
|
||||
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
|
||||
delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -72,12 +75,14 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::utils::{output_size, send_scale_transform};
|
||||
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;
|
||||
type PointerFocus = WlSurface;
|
||||
@@ -135,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.is_some_and(|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);
|
||||
@@ -225,12 +285,61 @@ 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();
|
||||
}
|
||||
|
||||
fn dropped(&mut self, _seat: Seat<Self>) {
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
let mut activate_output = true;
|
||||
if let Some(target) = validated.then_some(target).flatten() {
|
||||
if let Some(root) = self.niri.root_surface.get(&target) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
activate_output = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if activate_output {
|
||||
// Find the output from cursor coordinates.
|
||||
//
|
||||
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if !self.niri.pointer_hidden {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
self.niri.layout.activate_output(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -296,6 +405,10 @@ impl SessionLockHandler for State {
|
||||
|
||||
fn unlock(&mut self) {
|
||||
self.niri.unlock();
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
@@ -334,6 +447,7 @@ impl SecurityContextHandler for State {
|
||||
compositor_state: Default::default(),
|
||||
can_view_decoration_globals: config.prefer_no_csd,
|
||||
restricted: true,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
|
||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||
@@ -388,12 +502,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;
|
||||
}
|
||||
|
||||
@@ -403,7 +517,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,18 +665,22 @@ 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();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
|
||||
unmapped.activation_token_data = Some(token_data);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.activation_state.remove_token(&token);
|
||||
}
|
||||
}
|
||||
delegate_xdg_activation!(State);
|
||||
@@ -584,3 +702,5 @@ delegate_output_management!(State);
|
||||
|
||||
impl MutterX11InteropHandler for State {}
|
||||
delegate_mutter_x11_interop!(State);
|
||||
|
||||
delegate_single_pixel_buffer!(State);
|
||||
|
||||
+270
-96
@@ -23,12 +23,13 @@ use smithay::wayland::compositor::{
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::{self, Layer};
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
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::{
|
||||
@@ -36,9 +37,12 @@ use smithay::{
|
||||
};
|
||||
use tracing::field::Empty;
|
||||
|
||||
use crate::input::move_grab::MoveGrab;
|
||||
use crate::input::resize_grab::ResizeGrab;
|
||||
use crate::input::DOUBLE_CLICK_TIME;
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
use crate::input::touch_move_grab::TouchMoveGrab;
|
||||
use crate::input::touch_resize_grab::TouchResizeGrab;
|
||||
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
||||
use crate::layout::scrolling::ColumnWidth;
|
||||
use crate::niri::{PopupGrabState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
|
||||
@@ -65,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(
|
||||
@@ -76,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;
|
||||
};
|
||||
@@ -104,8 +209,16 @@ impl XdgShellHandler for State {
|
||||
// See if we got a double resize-click gesture.
|
||||
let time = get_monotonic_time();
|
||||
let last_cell = mapped.last_interactive_resize_start();
|
||||
let last = last_cell.get();
|
||||
let mut last = last_cell.get();
|
||||
last_cell.set(Some((time, edges)));
|
||||
|
||||
// Floating windows don't have either of the double-resize-click gestures, so just allow it
|
||||
// to resize.
|
||||
if mapped.is_floating() {
|
||||
last = None;
|
||||
last_cell.set(None);
|
||||
}
|
||||
|
||||
if let Some((last_time, last_edges)) = last {
|
||||
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
|
||||
// Allow quick resize after a triple click.
|
||||
@@ -128,14 +241,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(
|
||||
@@ -165,6 +289,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
let popup = PopupKind::Xdg(surface);
|
||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||
trace!("ignoring popup grab because no root surface");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -173,30 +298,30 @@ impl XdgShellHandler for State {
|
||||
// keyboard focus being at the wrong place.
|
||||
if self.niri.is_locked() {
|
||||
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
|
||||
trace!("ignoring popup grab because the session is locked");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
} else if self.niri.screenshot_ui.is_open() {
|
||||
trace!("ignoring popup grab because the screenshot UI is open");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
} else if let Some(output) = self.niri.layout.active_output() {
|
||||
let layers = layer_map_for_output(output);
|
||||
|
||||
if let Some(layer_surface) =
|
||||
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
if layers
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.is_none()
|
||||
{
|
||||
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
// This is a grab for a regular window; check that there's no layer surface with a
|
||||
// higher input priority.
|
||||
|
||||
// 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()
|
||||
}) {
|
||||
trace!("ignoring toplevel popup grab because the overlay layer has focus");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
@@ -209,33 +334,51 @@ impl XdgShellHandler for State {
|
||||
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|
||||
})
|
||||
{
|
||||
trace!("ignoring toplevel popup grab because the top layer has focus");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
|
||||
let layout_focus = self.niri.layout.focus();
|
||||
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
|
||||
trace!("ignoring toplevel popup grab because another window has focus");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trace!("ignoring popup grab because no output is active");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
|
||||
let seat = &self.niri.seat;
|
||||
let Ok(mut grab) = self
|
||||
let mut grab = match self
|
||||
.niri
|
||||
.popups
|
||||
.grab_popup(root.clone(), popup, seat, serial)
|
||||
else {
|
||||
return;
|
||||
{
|
||||
Ok(grab) => grab,
|
||||
Err(err) => {
|
||||
trace!("ignoring popup grab: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let keyboard = seat.get_keyboard().unwrap();
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
let can_receive_keyboard_focus = self
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.and_then(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
||||
&& !(keyboard.has_grab(serial)
|
||||
|| grab
|
||||
@@ -244,16 +387,22 @@ impl XdgShellHandler for State {
|
||||
let pointer_grab_mismatches = pointer.is_grabbed()
|
||||
&& !(pointer.has_grab(serial)
|
||||
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
||||
if keyboard_grab_mismatches || pointer_grab_mismatches {
|
||||
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
|
||||
trace!("ignoring popup grab because of current grab mismatch");
|
||||
grab.ungrab(PopupUngrabStrategy::All);
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("new grab for root {:?}", root);
|
||||
keyboard.set_focus(self, grab.current_grab(), serial);
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
|
||||
if can_receive_keyboard_focus {
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
|
||||
}
|
||||
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
|
||||
self.niri.popup_grab = Some(PopupGrabState { root, grab });
|
||||
self.niri.popup_grab = Some(PopupGrabState {
|
||||
root,
|
||||
grab,
|
||||
has_keyboard_grab: can_receive_keyboard_focus,
|
||||
});
|
||||
}
|
||||
|
||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||
@@ -261,7 +410,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();
|
||||
}
|
||||
}
|
||||
@@ -288,7 +437,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +481,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
|
||||
@@ -343,7 +492,7 @@ impl XdgShellHandler for State {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
ws.configure_new_window(&unmapped.window, None, rules);
|
||||
ws.configure_new_window(&unmapped.window, None, None, false, rules);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
@@ -367,6 +516,9 @@ impl XdgShellHandler for State {
|
||||
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
//
|
||||
// FIXME: when unfullscreening to floating, this will send an extra configure with
|
||||
// scrolling layout bounds. We should probably avoid it.
|
||||
toplevel.send_configure();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
match &mut unmapped.state {
|
||||
@@ -378,6 +530,9 @@ impl XdgShellHandler for State {
|
||||
InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
height,
|
||||
floating_width,
|
||||
floating_height,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
@@ -416,7 +571,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
|
||||
@@ -432,12 +587,26 @@ impl XdgShellHandler for State {
|
||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
|
||||
let configure_width = if *is_full_width {
|
||||
let is_floating = rules.compute_open_floating(&toplevel);
|
||||
let configure_width = if is_floating {
|
||||
*floating_width
|
||||
} else if *is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
} else {
|
||||
*width
|
||||
};
|
||||
ws.configure_new_window(&unmapped.window, configure_width, rules);
|
||||
let configure_height = if is_floating {
|
||||
*floating_height
|
||||
} else {
|
||||
*height
|
||||
};
|
||||
ws.configure_new_window(
|
||||
&unmapped.window,
|
||||
configure_width,
|
||||
configure_height,
|
||||
is_floating,
|
||||
rules,
|
||||
);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
@@ -493,7 +662,7 @@ impl XdgShellHandler for State {
|
||||
.start_close_animation_for_window(renderer, &window, blocker);
|
||||
});
|
||||
|
||||
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
@@ -525,6 +694,22 @@ impl XdgShellHandler for State {
|
||||
fn title_changed(&mut self, toplevel: ToplevelSurface) {
|
||||
self.update_window_rules(&toplevel);
|
||||
}
|
||||
|
||||
fn parent_changed(&mut self, toplevel: ToplevelSurface) {
|
||||
let Some(parent) = toplevel.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
|
||||
let output = output.cloned();
|
||||
let window = mapped.window.clone();
|
||||
if self.niri.layout.descendants_added(&window) {
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_xdg_shell!(State);
|
||||
@@ -552,7 +737,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();
|
||||
}
|
||||
}
|
||||
@@ -565,7 +750,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();
|
||||
}
|
||||
}
|
||||
@@ -620,18 +805,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");
|
||||
@@ -648,7 +821,7 @@ impl State {
|
||||
self.niri.is_at_startup,
|
||||
);
|
||||
|
||||
let Unmapped { window, state } = unmapped;
|
||||
let Unmapped { window, state, .. } = unmapped;
|
||||
|
||||
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
|
||||
error!("window must not be already configured in send_initial_configure()");
|
||||
@@ -706,11 +879,15 @@ 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;
|
||||
let mut floating_width = None;
|
||||
let mut height = None;
|
||||
let mut floating_height = None;
|
||||
let is_full_width = rules.open_maximized.unwrap_or(false);
|
||||
let is_floating = rules.compute_open_floating(toplevel);
|
||||
|
||||
// Tell the surface the preferred size and bounds for its likely output.
|
||||
let ws = rules
|
||||
@@ -732,14 +909,26 @@ impl State {
|
||||
});
|
||||
}
|
||||
|
||||
width = ws.resolve_default_width(rules.default_width);
|
||||
width = ws.resolve_default_width(rules.default_width, false);
|
||||
floating_width = ws.resolve_default_width(rules.default_width, true);
|
||||
height = ws.resolve_default_height(rules.default_height, false);
|
||||
floating_height = ws.resolve_default_height(rules.default_height, true);
|
||||
|
||||
let configure_width = if is_full_width {
|
||||
let configure_width = if is_floating {
|
||||
floating_width
|
||||
} else if is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
} else {
|
||||
width
|
||||
};
|
||||
ws.configure_new_window(window, configure_width, &rules);
|
||||
let configure_height = if is_floating { floating_height } else { height };
|
||||
ws.configure_new_window(
|
||||
window,
|
||||
configure_width,
|
||||
configure_height,
|
||||
is_floating,
|
||||
&rules,
|
||||
);
|
||||
}
|
||||
|
||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
||||
@@ -757,9 +946,12 @@ impl State {
|
||||
*state = InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
height,
|
||||
floating_width,
|
||||
floating_height,
|
||||
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();
|
||||
@@ -788,16 +980,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();
|
||||
@@ -833,8 +1016,8 @@ impl State {
|
||||
};
|
||||
|
||||
// Figure out if the root is a window or a layer surface.
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
|
||||
self.unconstrain_window_popup(popup, &mapped.window, output);
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||
self.unconstrain_window_popup(popup, &mapped.window);
|
||||
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
@@ -844,19 +1027,10 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
|
||||
let window_geo = window.geometry();
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
|
||||
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) {
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
//
|
||||
// We try to keep regular window popups within the window itself horizontally (since the
|
||||
// window can be scrolled to both edges of the screen), but within the whole monitor's
|
||||
// height.
|
||||
let mut target =
|
||||
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
|
||||
target.loc -= self.niri.layout.window_loc(window).unwrap();
|
||||
let mut target = self.niri.layout.popup_target_rect(window);
|
||||
target.loc -= get_popup_toplevel_coords(popup).to_f64();
|
||||
|
||||
self.position_popup_within_rect(popup, target);
|
||||
@@ -876,7 +1050,7 @@ impl State {
|
||||
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
|
||||
let mut target = Rectangle::from_size(output_geo.size);
|
||||
target.loc -= layer_geo.loc;
|
||||
target.loc -= get_popup_toplevel_coords(popup);
|
||||
|
||||
@@ -921,7 +1095,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
||||
pub fn update_reactive_popups(&self, window: &Window) {
|
||||
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
||||
|
||||
for (popup, _) in PopupManager::popups_for_surface(
|
||||
@@ -930,7 +1104,7 @@ impl State {
|
||||
match &popup {
|
||||
xdg_popup @ PopupKind::Xdg(popup) => {
|
||||
if popup.with_pending_state(|state| state.positioner.reactive) {
|
||||
self.unconstrain_window_popup(xdg_popup, window, output);
|
||||
self.unconstrain_window_popup(xdg_popup, window);
|
||||
if let Err(err) = popup.send_pending_configure() {
|
||||
warn!("error re-configuring reactive popup: {err:?}");
|
||||
}
|
||||
|
||||
+691
-109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// When moving with the left button, right toggles floating, and vice versa.
|
||||
let toggle_floating_button = if self.start_data.button == 0x110 {
|
||||
0x111
|
||||
} else {
|
||||
0x110
|
||||
};
|
||||
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
|
||||
data.niri.layout.toggle_window_floating(Some(&self.window));
|
||||
}
|
||||
|
||||
if !handle.current_pressed().contains(&self.start_data.button) {
|
||||
// The button that initiated the grab was released.
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
@@ -23,6 +26,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
},
|
||||
Msg::Workspaces => Request::Workspaces,
|
||||
Msg::Windows => Request::Windows,
|
||||
Msg::Layers => Request::Layers,
|
||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
@@ -168,6 +172,69 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::Layers => {
|
||||
let Response::Layers(mut layers) = response else {
|
||||
bail!("unexpected response: expected Layers, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let layers = serde_json::to_string(&layers).context("error formatting response")?;
|
||||
println!("{layers}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
layers.sort_by(|a, b| {
|
||||
Ord::cmp(&a.output, &b.output)
|
||||
.then_with(|| Ord::cmp(&a.layer, &b.layer))
|
||||
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
|
||||
});
|
||||
let mut iter = layers.iter().peekable();
|
||||
|
||||
let print = |surface: &niri_ipc::LayerSurface| {
|
||||
println!(" Surface:");
|
||||
println!(" Namespace: \"{}\"", &surface.namespace);
|
||||
|
||||
let interactivity = match surface.keyboard_interactivity {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
|
||||
};
|
||||
println!(" Keyboard interactivity: {interactivity}");
|
||||
};
|
||||
|
||||
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
|
||||
output: &str,
|
||||
layer| {
|
||||
let mut empty = true;
|
||||
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
|
||||
empty = false;
|
||||
println!();
|
||||
print(surface);
|
||||
}
|
||||
if empty {
|
||||
println!(" (empty)\n");
|
||||
} else {
|
||||
println!();
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(surface) = iter.peek() {
|
||||
let output = &surface.output;
|
||||
println!("Output \"{output}\":");
|
||||
|
||||
print!(" Background layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Background);
|
||||
|
||||
print!(" Bottom layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
|
||||
|
||||
print!(" Top layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Top);
|
||||
|
||||
print!(" Overlay layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
|
||||
}
|
||||
}
|
||||
Msg::FocusedOutput => {
|
||||
let Response::FocusedOutput(output) = response else {
|
||||
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
||||
@@ -449,6 +516,17 @@ fn print_window(window: &Window) {
|
||||
println!(" App ID: (unset)");
|
||||
}
|
||||
|
||||
println!(
|
||||
" Is floating: {}",
|
||||
if window.is_floating { "yes" } else { "no" }
|
||||
);
|
||||
|
||||
if let Some(pid) = window.pid {
|
||||
println!(" PID: {pid}");
|
||||
} else {
|
||||
println!(" PID: (unknown)");
|
||||
}
|
||||
|
||||
if let Some(workspace_id) = window.workspace_id {
|
||||
println!(" Workspace ID: {workspace_id}");
|
||||
} else {
|
||||
|
||||
+75
-41
@@ -16,17 +16,16 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use smithay::input::keyboard::XkbContextHandler;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
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 smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||
|
||||
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
|
||||
@@ -257,6 +256,47 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let windows = state.windows.windows.values().cloned().collect();
|
||||
Response::Windows(windows)
|
||||
}
|
||||
Request::Layers => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let mut layers = Vec::new();
|
||||
for output in state.niri.global_space.outputs() {
|
||||
let name = output.name();
|
||||
for surface in layer_map_for_output(output).layers() {
|
||||
let layer = match surface.layer() {
|
||||
Layer::Background => niri_ipc::Layer::Background,
|
||||
Layer::Bottom => niri_ipc::Layer::Bottom,
|
||||
Layer::Top => niri_ipc::Layer::Top,
|
||||
Layer::Overlay => niri_ipc::Layer::Overlay,
|
||||
};
|
||||
let keyboard_interactivity =
|
||||
match surface.cached_state().keyboard_interactivity {
|
||||
KeyboardInteractivity::None => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None
|
||||
}
|
||||
KeyboardInteractivity::Exclusive => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
|
||||
}
|
||||
KeyboardInteractivity::OnDemand => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
|
||||
}
|
||||
};
|
||||
|
||||
layers.push(niri_ipc::LayerSurface {
|
||||
namespace: surface.namespace().to_owned(),
|
||||
output: name.clone(),
|
||||
layer,
|
||||
keyboard_interactivity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send_blocking(layers);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let layers = result.map_err(|_| String::from("error getting layers info"))?;
|
||||
Response::Layers(layers)
|
||||
}
|
||||
Request::KeyboardLayouts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
||||
@@ -274,6 +314,9 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
// Make sure some logic like workspace clean-up has a chance to run before doing
|
||||
// actions.
|
||||
state.niri.advance_animations();
|
||||
state.do_action(action, false);
|
||||
let _ = tx.send_blocking(());
|
||||
});
|
||||
@@ -362,22 +405,14 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result
|
||||
}
|
||||
|
||||
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
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(),
|
||||
}
|
||||
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
pid: mapped.credentials().map(|c| c.pid),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
is_floating: mapped.is_floating(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -385,10 +420,13 @@ 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 layouts = context.keymap().layouts();
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
let layouts = xkb.layouts();
|
||||
KeyboardLayouts {
|
||||
names: layouts.map(str::to_owned).collect(),
|
||||
current_idx: context.active_layout().0 as u8,
|
||||
names: layouts
|
||||
.map(|layout| xkb.layout_name(layout).to_owned())
|
||||
.collect(),
|
||||
current_idx: xkb.active_layout().0 as u8,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -406,7 +444,10 @@ impl State {
|
||||
|
||||
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| context.active_layout().0 as u8);
|
||||
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;
|
||||
@@ -459,7 +500,7 @@ impl State {
|
||||
// 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 != ws.name
|
||||
|| ipc_ws.name.as_ref() != ws.name()
|
||||
|| ipc_ws.output.as_ref() != output_name
|
||||
{
|
||||
need_workspaces_changed = true;
|
||||
@@ -482,7 +523,7 @@ impl State {
|
||||
}
|
||||
|
||||
// Check if this workspace became active.
|
||||
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx);
|
||||
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
|
||||
if is_active && !ipc_ws.is_active {
|
||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||
}
|
||||
@@ -503,9 +544,9 @@ impl State {
|
||||
Workspace {
|
||||
id,
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name.clone(),
|
||||
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_active: mon.is_some_and(|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()),
|
||||
}
|
||||
@@ -546,28 +587,21 @@ impl State {
|
||||
}
|
||||
|
||||
let Some(ipc_win) = state.windows.get(&id) else {
|
||||
let window = make_ipc_window(mapped, Some(ws_id));
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace_id = Some(ws_id.get());
|
||||
let mut changed = ipc_win.workspace_id != workspace_id;
|
||||
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
changed |= with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
let workspace_id = ws_id.map(|id| id.get());
|
||||
let mut changed =
|
||||
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
|
||||
|
||||
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, Some(ws_id));
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use niri_config::layer_rule::LayerRule;
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Rectangle, Scale};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MappedLayer {
|
||||
/// The surface itself.
|
||||
surface: LayerSurface,
|
||||
|
||||
/// Up-to-date rules.
|
||||
rules: ResolvedLayerRules,
|
||||
|
||||
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||
block_out_buffer: RefCell<SolidColorBuffer>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
LayerSurfaceRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
|
||||
Self {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &LayerSurface {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> &ResolvedLayerRules {
|
||||
&self.rules
|
||||
}
|
||||
|
||||
/// Recomputes the resolved layer rules and returns whether they changed.
|
||||
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
|
||||
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
|
||||
if new_rules == self.rules {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.rules = new_rules;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
let geometry = geometry
|
||||
.to_f64()
|
||||
.to_physical_precise_round(scale)
|
||||
.to_logical(scale);
|
||||
|
||||
let mut buffer = self.block_out_buffer.borrow_mut();
|
||||
buffer.resize(geometry.size.to_f64());
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&buffer,
|
||||
geometry.loc,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = geometry.loc;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
));
|
||||
}
|
||||
|
||||
rv.normal = render_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use niri_config::layer_rule::{LayerRule, Match};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::desktop::LayerSurface;
|
||||
|
||||
pub mod mapped;
|
||||
pub use mapped::MappedLayer;
|
||||
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this window with.
|
||||
pub opacity: Option<f32>,
|
||||
/// Whether to block out this window from certain render targets.
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||
|
||||
let mut resolved = ResolvedLayerRules::empty();
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m: &Match| {
|
||||
if let Some(at_startup) = m.at_startup {
|
||||
if at_startup != is_at_startup {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
surface_matches(surface, m)
|
||||
};
|
||||
|
||||
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.excludes.iter().any(matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(x) = rule.opacity {
|
||||
resolved.opacity = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.block_out_from {
|
||||
resolved.block_out_from = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
resolved
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
|
||||
if let Some(namespace_re) = &m.namespace {
|
||||
if !namespace_re.0.is_match(surface.namespace()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -138,16 +137,15 @@ impl ClosingWindow {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
pub fn advance_animations(&mut self) {
|
||||
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);
|
||||
let anim = anim.restarted(0., 1., 0.);
|
||||
self.anim_state = AnimationState::Animating(anim);
|
||||
}
|
||||
}
|
||||
AnimationState::Animating(anim) => anim.set_current_time(current_time),
|
||||
AnimationState::Animating(_anim) => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,7 @@ impl FocusRing {
|
||||
in_: GradientInterpolation::default(),
|
||||
});
|
||||
|
||||
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
|
||||
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
|
||||
let gradient_area = match gradient.relative_to {
|
||||
GradientRelativeTo::Window => full_rect,
|
||||
GradientRelativeTo::WorkspaceView => view_rect,
|
||||
@@ -178,12 +178,12 @@ impl FocusRing {
|
||||
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
||||
border.update(
|
||||
size,
|
||||
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
|
||||
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
|
||||
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),
|
||||
Rectangle::new(full_rect.loc - loc, full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
@@ -196,15 +196,12 @@ impl FocusRing {
|
||||
|
||||
self.borders[0].update(
|
||||
self.sizes[0],
|
||||
Rectangle::from_loc_and_size(
|
||||
gradient_area.loc - self.locations[0],
|
||||
gradient_area.size,
|
||||
),
|
||||
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
|
||||
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),
|
||||
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+3183
-902
File diff suppressed because it is too large
Load Diff
+446
-439
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -41,9 +40,7 @@ impl OpenAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.anim.set_current_time(current_time);
|
||||
}
|
||||
pub fn advance_animations(&mut self) {}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.anim.is_done()
|
||||
@@ -75,7 +72,7 @@ impl OpenAnimation {
|
||||
let texture_size = geo.size.to_f64().to_logical(scale);
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
|
||||
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
|
||||
let mut area = Rectangle::new(location + offset, texture_size);
|
||||
|
||||
// Expand the area a bit to allow for more varied effects.
|
||||
let mut target_size = area.size.upscale(1.5);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+156
-44
@@ -1,5 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
@@ -10,10 +9,10 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
|
||||
use super::{
|
||||
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
|
||||
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac,
|
||||
RESIZE_ANIMATION_THRESHOLD,
|
||||
};
|
||||
use crate::animation::Animation;
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
|
||||
@@ -49,8 +48,27 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The black backdrop for fullscreen windows.
|
||||
fullscreen_backdrop: SolidColorBuffer,
|
||||
|
||||
/// The size we were requested to fullscreen into.
|
||||
fullscreen_size: Size<f64, Logical>,
|
||||
/// Whether the tile should float upon unfullscreening.
|
||||
pub(super) unfullscreen_to_floating: bool,
|
||||
|
||||
/// The size that the window should assume when going floating.
|
||||
///
|
||||
/// This is generally the last size the window had when it was floating. It can be unknown if
|
||||
/// the window starts out in the tiling layout or fullscreen.
|
||||
pub(super) floating_window_size: Option<Size<i32, Logical>>,
|
||||
|
||||
/// The position that the tile should assume when going floating, relative to the floating
|
||||
/// space working area.
|
||||
///
|
||||
/// This is generally the last position the tile had when it was floating. It can be unknown if
|
||||
/// the window starts out in the tiling layout.
|
||||
pub(super) floating_pos: Option<Point<f64, SizeFrac>>,
|
||||
|
||||
/// Currently selected preset width index when this tile is floating.
|
||||
pub(super) floating_preset_width_idx: Option<usize>,
|
||||
|
||||
/// Currently selected preset height index when this tile is floating.
|
||||
pub(super) floating_preset_height_idx: Option<usize>,
|
||||
|
||||
/// The animation upon opening a window.
|
||||
open_animation: Option<OpenAnimation>,
|
||||
@@ -64,17 +82,28 @@ 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>,
|
||||
|
||||
/// Extra damage for clipped surface corner radius changes.
|
||||
rounded_corner_damage: RoundedCornerDamage,
|
||||
|
||||
/// The view size for the tile's workspace.
|
||||
///
|
||||
/// Used as the fullscreen target size.
|
||||
view_size: Size<f64, Logical>,
|
||||
|
||||
/// Scale of the output the tile is on (and rounds its sizes to).
|
||||
scale: f64,
|
||||
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
pub(super) options: Rc<Options>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
@@ -90,7 +119,7 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
type TileRenderSnapshot =
|
||||
pub type TileRenderSnapshot =
|
||||
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -107,30 +136,58 @@ struct MoveAnimation {
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Tile<W> {
|
||||
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
|
||||
pub fn new(
|
||||
window: W,
|
||||
view_size: Size<f64, Logical>,
|
||||
scale: f64,
|
||||
clock: Clock,
|
||||
options: Rc<Options>,
|
||||
) -> Self {
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(options.border);
|
||||
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
|
||||
let is_fullscreen = window.is_fullscreen();
|
||||
|
||||
Self {
|
||||
window,
|
||||
border: FocusRing::new(border_config.into()),
|
||||
focus_ring: FocusRing::new(focus_ring_config.into()),
|
||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
fullscreen_size: Default::default(),
|
||||
is_fullscreen,
|
||||
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
|
||||
unfullscreen_to_floating: false,
|
||||
floating_window_size: None,
|
||||
floating_pos: None,
|
||||
floating_preset_width_idx: None,
|
||||
floating_preset_height_idx: None,
|
||||
open_animation: None,
|
||||
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(),
|
||||
view_size,
|
||||
scale,
|
||||
clock,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
|
||||
pub fn update_config(
|
||||
&mut self,
|
||||
view_size: Size<f64, Logical>,
|
||||
scale: f64,
|
||||
options: Rc<Options>,
|
||||
) {
|
||||
// If preset widths or heights changed, clear our stored preset index.
|
||||
if self.options.preset_column_widths != options.preset_column_widths {
|
||||
self.floating_preset_width_idx = None;
|
||||
}
|
||||
if self.options.preset_window_heights != options.preset_window_heights {
|
||||
self.floating_preset_height_idx = None;
|
||||
}
|
||||
|
||||
self.view_size = view_size;
|
||||
self.scale = scale;
|
||||
self.options = options;
|
||||
|
||||
@@ -143,6 +200,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.focus_ring
|
||||
.resolve_against(self.options.focus_ring.into());
|
||||
self.focus_ring.update_config(focus_ring_config.into());
|
||||
|
||||
self.fullscreen_backdrop.resize(view_size);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
@@ -151,10 +210,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
pub fn update_window(&mut self) {
|
||||
// FIXME: remove when we can get a fullscreen size right away.
|
||||
if self.fullscreen_size != Size::from((0., 0.)) {
|
||||
self.is_fullscreen = self.window.is_fullscreen();
|
||||
}
|
||||
self.is_fullscreen = self.window.is_fullscreen();
|
||||
|
||||
if let Some(animate_from) = self.window.take_animation_snapshot() {
|
||||
let size_from = if let Some(resize) = self.resize_animation.take() {
|
||||
@@ -176,7 +232,13 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let change = self.window.size().to_f64().to_point() - size_from.to_point();
|
||||
let change = f64::max(change.x.abs(), change.y.abs());
|
||||
if change > RESIZE_ANIMATION_THRESHOLD {
|
||||
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
|
||||
let anim = Animation::new(
|
||||
self.clock.clone(),
|
||||
0.,
|
||||
1.,
|
||||
0.,
|
||||
self.options.animations.window_resize.anim,
|
||||
);
|
||||
self.resize_animation = Some(ResizeAnimation {
|
||||
anim,
|
||||
size_from,
|
||||
@@ -204,29 +266,25 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.rounded_corner_damage.set_size(window_size);
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
pub fn advance_animations(&mut self) {
|
||||
if let Some(open) = &mut self.open_animation {
|
||||
open.advance_animations(current_time);
|
||||
if open.is_done() {
|
||||
self.open_animation = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(resize) = &mut self.resize_animation {
|
||||
resize.anim.set_current_time(current_time);
|
||||
if resize.anim.is_done() {
|
||||
self.resize_animation = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(move_) = &mut self.move_x_animation {
|
||||
move_.anim.set_current_time(current_time);
|
||||
if move_.anim.is_done() {
|
||||
self.move_x_animation = None;
|
||||
}
|
||||
}
|
||||
if let Some(move_) = &mut self.move_y_animation {
|
||||
move_.anim.set_current_time(current_time);
|
||||
if move_.anim.is_done() {
|
||||
self.move_y_animation = None;
|
||||
}
|
||||
@@ -260,7 +318,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.animated_window_size(),
|
||||
is_active,
|
||||
!draw_border_with_background,
|
||||
Rectangle::from_loc_and_size(
|
||||
Rectangle::new(
|
||||
view_rect.loc - Point::from((border_width, border_width)),
|
||||
view_rect.size,
|
||||
),
|
||||
@@ -305,11 +363,14 @@ impl<W: LayoutElement> Tile<W> {
|
||||
offset.y += move_.from * move_.anim.value();
|
||||
}
|
||||
|
||||
offset += self.interactive_move_offset;
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self) {
|
||||
self.open_animation = Some(OpenAnimation::new(Animation::new(
|
||||
self.clock.clone(),
|
||||
0.,
|
||||
1.,
|
||||
0.,
|
||||
@@ -337,7 +398,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let anim = self.move_x_animation.take().map(|move_| move_.anim);
|
||||
let anim = anim
|
||||
.map(|anim| anim.restarted(1., 0., 0.))
|
||||
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
||||
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
|
||||
|
||||
self.move_x_animation = Some(MoveAnimation {
|
||||
anim,
|
||||
@@ -356,7 +417,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let anim = self.move_y_animation.take().map(|move_| move_.anim);
|
||||
let anim = anim
|
||||
.map(|anim| anim.restarted(1., 0., 0.))
|
||||
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
||||
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
|
||||
|
||||
self.move_y_animation = Some(MoveAnimation {
|
||||
anim,
|
||||
@@ -364,6 +425,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
|
||||
}
|
||||
@@ -372,16 +438,12 @@ impl<W: LayoutElement> Tile<W> {
|
||||
&mut self.window
|
||||
}
|
||||
|
||||
pub fn into_window(self) -> W {
|
||||
self.window
|
||||
}
|
||||
|
||||
pub fn is_fullscreen(&self) -> bool {
|
||||
self.is_fullscreen
|
||||
}
|
||||
|
||||
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
||||
fn effective_border_width(&self) -> Option<f64> {
|
||||
pub fn effective_border_width(&self) -> Option<f64> {
|
||||
if self.is_fullscreen {
|
||||
return None;
|
||||
}
|
||||
@@ -400,7 +462,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// In fullscreen, center the window in the given size.
|
||||
if self.is_fullscreen {
|
||||
let window_size = self.window_size();
|
||||
let target_size = self.fullscreen_size;
|
||||
let target_size = self.view_size;
|
||||
|
||||
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
|
||||
// one, leave it at the top-left as usual.
|
||||
@@ -430,8 +492,27 @@ impl<W: LayoutElement> Tile<W> {
|
||||
if self.is_fullscreen {
|
||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||
size.w = f64::max(size.w, self.fullscreen_size.w);
|
||||
size.h = f64::max(size.h, self.fullscreen_size.h);
|
||||
size.w = f64::max(size.w, self.view_size.w);
|
||||
size.h = f64::max(size.h, self.view_size.h);
|
||||
return size;
|
||||
}
|
||||
|
||||
if let Some(width) = self.effective_border_width() {
|
||||
size.w += width * 2.;
|
||||
size.h += width * 2.;
|
||||
}
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window_expected_or_current_size();
|
||||
|
||||
if self.is_fullscreen {
|
||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||
size.w = f64::max(size.w, self.view_size.w);
|
||||
size.h = f64::max(size.h, self.view_size.h);
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -451,6 +532,15 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size
|
||||
}
|
||||
|
||||
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
|
||||
let size = self.window.expected_size();
|
||||
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
|
||||
size = size
|
||||
.to_physical_precise_round(self.scale)
|
||||
.to_logical(self.scale);
|
||||
size
|
||||
}
|
||||
|
||||
fn animated_window_size(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window_size();
|
||||
|
||||
@@ -474,8 +564,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
if self.is_fullscreen {
|
||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||
size.w = f64::max(size.w, self.fullscreen_size.w);
|
||||
size.h = f64::max(size.h, self.fullscreen_size.h);
|
||||
size.w = f64::max(size.w, self.view_size.w);
|
||||
size.h = f64::max(size.h, self.view_size.h);
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -500,7 +590,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
||||
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
|
||||
let activation_region = Rectangle::from_size(self.tile_size());
|
||||
activation_region.contains(point)
|
||||
}
|
||||
|
||||
@@ -556,10 +646,9 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
|
||||
self.fullscreen_backdrop.resize(size);
|
||||
self.fullscreen_size = size;
|
||||
self.window.request_fullscreen(size.to_i32_round());
|
||||
pub fn request_fullscreen(&mut self) {
|
||||
self.window
|
||||
.request_fullscreen(self.view_size.to_i32_round());
|
||||
}
|
||||
|
||||
pub fn min_size(&self) -> Size<f64, Logical> {
|
||||
@@ -622,7 +711,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let window_size = self.window_size().to_f64();
|
||||
let animated_window_size = self.animated_window_size();
|
||||
let window_render_loc = location + window_loc;
|
||||
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
|
||||
let area = Rectangle::new(window_render_loc, animated_window_size);
|
||||
|
||||
let rules = self.window.rules();
|
||||
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
|
||||
@@ -721,7 +810,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.window
|
||||
.render(renderer, window_render_loc, scale, alpha, target);
|
||||
|
||||
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
|
||||
let geo = Rectangle::new(window_render_loc, window_size);
|
||||
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
|
||||
|
||||
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
|
||||
@@ -763,12 +852,12 @@ impl<W: LayoutElement> Tile<W> {
|
||||
if radius != CornerRadius::default() && has_border_shader {
|
||||
return BorderRenderElement::new(
|
||||
geo.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
||||
Rectangle::from_size(geo.size),
|
||||
GradientInterpolation::default(),
|
||||
Color::from_color32f(elem.color()),
|
||||
Color::from_color32f(elem.color()),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
||||
Rectangle::from_size(geo.size),
|
||||
0.,
|
||||
radius,
|
||||
scale.x as f32,
|
||||
@@ -905,4 +994,27 @@ impl<W: LayoutElement> Tile<W> {
|
||||
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
|
||||
self.unmap_snapshot.take()
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &Rc<Options> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn view_size(&self) -> Size<f64, Logical> {
|
||||
self.view_size
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn verify_invariants(&self) {
|
||||
use approx::assert_abs_diff_eq;
|
||||
|
||||
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
|
||||
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
|
||||
|
||||
let scale = self.scale;
|
||||
let size = self.tile_size();
|
||||
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
|
||||
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
|
||||
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
+1105
-3471
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ pub mod frame_clock;
|
||||
pub mod handlers;
|
||||
pub mod input;
|
||||
pub mod ipc;
|
||||
pub mod layer;
|
||||
pub mod layout;
|
||||
pub mod niri;
|
||||
pub mod protocols;
|
||||
@@ -27,3 +28,6 @@ pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+8
-10
@@ -11,7 +11,6 @@ use std::{env, mem};
|
||||
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
use niri::animation;
|
||||
use niri::cli::{Cli, Sub};
|
||||
#[cfg(feature = "dbus")]
|
||||
use niri::dbus;
|
||||
@@ -33,6 +32,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() {
|
||||
@@ -159,13 +163,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let slowdown = if config.animations.off {
|
||||
0.
|
||||
} else {
|
||||
config.animations.slowdown.clamp(0., 100.)
|
||||
};
|
||||
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||
|
||||
@@ -179,6 +176,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
event_loop.handle(),
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -239,10 +237,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
};
|
||||
|
||||
// Spawn commands from cli and auto-start.
|
||||
spawn(cli.command);
|
||||
spawn(cli.command, None);
|
||||
|
||||
for elem in spawn_at_startup {
|
||||
spawn(elem.command);
|
||||
spawn(elem.command, None);
|
||||
}
|
||||
|
||||
// Show the config error notification right away if needed.
|
||||
|
||||
+646
-243
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;
|
||||
|
||||
@@ -96,37 +94,23 @@ pub fn refresh(state: &mut State) {
|
||||
// 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();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,6 @@ where
|
||||
}
|
||||
|
||||
// Verify that there's no more data.
|
||||
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
|
||||
{
|
||||
match file.read(&mut [0]) {
|
||||
Ok(0) => (),
|
||||
|
||||
@@ -206,9 +206,9 @@ where
|
||||
let output_transform = output.current_transform();
|
||||
let output_physical_size =
|
||||
output_transform.transform_size(output.current_mode().unwrap().size);
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
|
||||
let output_rect = Rectangle::from_size(output_physical_size);
|
||||
|
||||
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
|
||||
let rect = Rectangle::new(Point::from((x, y)), Size::from((width, height)));
|
||||
|
||||
let output_scale = output.current_scale().fractional_scale();
|
||||
let physical_rect = rect.to_physical_precise_round(output_scale);
|
||||
|
||||
+29
-22
@@ -11,7 +11,7 @@ use anyhow::Context as _;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use calloop::RegistrationToken;
|
||||
use pipewire::context::Context;
|
||||
use pipewire::core::Core;
|
||||
use pipewire::core::{Core, PW_ID_CORE};
|
||||
use pipewire::main_loop::MainLoop;
|
||||
use pipewire::properties::Properties;
|
||||
use pipewire::spa::buffer::DataType;
|
||||
@@ -41,7 +41,7 @@ use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Scale, Size, Transform};
|
||||
use zbus::SignalContext;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
||||
use crate::niri::State;
|
||||
@@ -54,12 +54,14 @@ const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
|
||||
pub struct PipeWire {
|
||||
_context: Context,
|
||||
pub core: Core,
|
||||
pub token: RegistrationToken,
|
||||
to_niri: calloop::channel::Sender<PwToNiri>,
|
||||
}
|
||||
|
||||
pub enum PwToNiri {
|
||||
StopCast { session_id: usize },
|
||||
Redraw(CastTarget),
|
||||
FatalError,
|
||||
}
|
||||
|
||||
pub struct Cast {
|
||||
@@ -134,15 +136,26 @@ macro_rules! make_params {
|
||||
}
|
||||
|
||||
impl PipeWire {
|
||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||
pub fn new(
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
to_niri: calloop::channel::Sender<PwToNiri>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
||||
let context = Context::new(&main_loop).context("error creating Context")?;
|
||||
let core = context.connect(None).context("error creating Core")?;
|
||||
|
||||
let to_niri_ = to_niri.clone();
|
||||
let listener = core
|
||||
.add_listener_local()
|
||||
.error(|id, seq, res, message| {
|
||||
.error(move |id, seq, res, message| {
|
||||
warn!(id, seq, res, message, "pw error");
|
||||
|
||||
// Reset PipeWire on connection errors.
|
||||
if id == PW_ID_CORE && res == -32 {
|
||||
if let Err(err) = to_niri_.send(PwToNiri::FatalError) {
|
||||
warn!("error sending FatalError to niri: {err:?}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.register();
|
||||
mem::forget(listener);
|
||||
@@ -154,7 +167,7 @@ impl PipeWire {
|
||||
}
|
||||
}
|
||||
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
let token = event_loop
|
||||
.insert_source(generic, move |_, wrapper, _| {
|
||||
let _span = tracy_client::span!("pipewire iteration");
|
||||
wrapper.0.loop_().iterate(Duration::ZERO);
|
||||
@@ -162,17 +175,10 @@ impl PipeWire {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (to_niri, from_pipewire) = calloop::channel::channel();
|
||||
event_loop
|
||||
.insert_source(from_pipewire, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(Self {
|
||||
_context: context,
|
||||
core,
|
||||
token,
|
||||
to_niri,
|
||||
})
|
||||
}
|
||||
@@ -188,7 +194,7 @@ impl PipeWire {
|
||||
refresh: u32,
|
||||
alpha: bool,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalContext<'static>,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
) -> anyhow::Result<Cast> {
|
||||
let _span = tracy_client::span!("PipeWire::start_cast");
|
||||
|
||||
@@ -769,7 +775,11 @@ impl Cast {
|
||||
let timer = Timer::from_duration(duration);
|
||||
let token = event_loop
|
||||
.insert_source(timer, move |_, _, state| {
|
||||
state.niri.queue_redraw(&output);
|
||||
// Guard against output disconnecting before the timer has a chance to run.
|
||||
if state.niri.output_state.contains_key(&output) {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
TimeoutAction::Drop
|
||||
})
|
||||
.unwrap();
|
||||
@@ -834,12 +844,9 @@ impl Cast {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut buffer = match self.stream.dequeue_buffer() {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
warn!("no available buffer in pw stream, skipping frame");
|
||||
return false;
|
||||
}
|
||||
let Some(mut buffer) = self.stream.dequeue_buffer() else {
|
||||
warn!("no available buffer in pw stream, skipping frame");
|
||||
return false;
|
||||
};
|
||||
|
||||
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||
@@ -1023,7 +1030,7 @@ fn allocate_buffer(
|
||||
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
|
||||
.context("error creating GBM buffer object")?;
|
||||
|
||||
let modifier = bo.modifier().unwrap();
|
||||
let modifier = bo.modifier();
|
||||
let buffer = GbmBuffer::from_bo(bo, false);
|
||||
Ok((buffer, modifier))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use smithay::backend::renderer::gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
|
||||
};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::damage::ExtraDamage;
|
||||
use super::renderer::{AsGlesFrame as _, NiriRenderer};
|
||||
@@ -117,21 +117,21 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
let bottom_left = corner_radius.bottom_left as f64;
|
||||
|
||||
[
|
||||
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
|
||||
Rectangle::from_loc_and_size(
|
||||
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
|
||||
(top_right, top_right),
|
||||
Rectangle::new(geo.loc, Size::from((top_left, top_left))),
|
||||
Rectangle::new(
|
||||
Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)),
|
||||
Size::from((top_right, top_right)),
|
||||
),
|
||||
Rectangle::from_loc_and_size(
|
||||
(
|
||||
Rectangle::new(
|
||||
Point::from((
|
||||
geo.loc.x + geo.size.w - bottom_right,
|
||||
geo.loc.y + geo.size.h - bottom_right,
|
||||
),
|
||||
(bottom_right, bottom_right),
|
||||
)),
|
||||
Size::from((bottom_right, bottom_right)),
|
||||
),
|
||||
Rectangle::from_loc_and_size(
|
||||
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
|
||||
(bottom_left, bottom_left),
|
||||
Rectangle::new(
|
||||
Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)),
|
||||
Size::from((bottom_left, bottom_left)),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ impl Element for ExtraDamage {
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
|
||||
Rectangle::from_size(Size::from((1., 1.)))
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
|
||||
@@ -113,6 +113,11 @@ impl<E> SplitElements<E> {
|
||||
popups.extend(normal);
|
||||
popups
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: SplitElements<E>) {
|
||||
self.popups.extend(other.popups);
|
||||
self.normal.extend(other.normal);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
|
||||
@@ -211,7 +216,7 @@ pub fn render_and_download(
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
|
||||
.copy_framebuffer(Rectangle::from_size(buffer_size), fourcc)
|
||||
.context("error copying framebuffer")?;
|
||||
Ok(mapping)
|
||||
}
|
||||
@@ -295,7 +300,7 @@ fn render_elements(
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<SyncPoint> {
|
||||
let transform = transform.invert();
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
|
||||
let output_rect = Rectangle::from_size(transform.transform_size(size));
|
||||
|
||||
let mut frame = renderer
|
||||
.render(size, transform)
|
||||
|
||||
@@ -46,7 +46,7 @@ impl AsGlesRenderer for GlesRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
|
||||
impl AsGlesRenderer for TtyRenderer<'_> {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self.as_mut()
|
||||
}
|
||||
@@ -66,7 +66,7 @@ impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
|
||||
impl<'frame> AsGlesFrame<'frame> for TtyFrame<'_, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self.as_mut()
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ impl ResizeRenderElement {
|
||||
let tex_next_geo_scaled = tex_next_geo.to_f64().upscale(scale_next);
|
||||
let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled).to_i32_up();
|
||||
|
||||
let area = Rectangle::from_loc_and_size(
|
||||
let area = Rectangle::new(
|
||||
area.loc + combined_geo.loc.to_logical(scale),
|
||||
combined_geo.size.to_logical(scale),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
@@ -219,11 +193,11 @@ impl ShaderRenderElement {
|
||||
program,
|
||||
id: Id::new(),
|
||||
commit_counter: CommitCounter::default(),
|
||||
area: Rectangle::from_loc_and_size((0., 0.), size),
|
||||
area: Rectangle::from_size(size),
|
||||
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();
|
||||
@@ -281,7 +255,7 @@ impl Element for ShaderRenderElement {
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
|
||||
Rectangle::from_size(Size::from((1., 1.)))
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
@@ -340,7 +314,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||
);
|
||||
|
||||
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
|
||||
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
|
||||
[
|
||||
rect.loc.x as f32,
|
||||
rect.loc.y as f32,
|
||||
@@ -360,7 +334,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||
);
|
||||
|
||||
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
|
||||
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
|
||||
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
|
||||
(0..6).flat_map(move |_| {
|
||||
[
|
||||
|
||||
@@ -85,7 +85,7 @@ impl SolidColorRenderElement {
|
||||
alpha: f32,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
let geo = Rectangle::from_loc_and_size(location, buffer.size());
|
||||
let geo = Rectangle::new(location.into(), buffer.size());
|
||||
let color = buffer.color * alpha;
|
||||
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl Element for SolidColorRenderElement {
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
|
||||
Rectangle::from_size(Size::from((1., 1.)))
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
@@ -134,8 +134,7 @@ impl Element for SolidColorRenderElement {
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
if self.color.is_opaque() {
|
||||
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
|
||||
.to_physical_precise_down(scale);
|
||||
let rect = Rectangle::from_size(self.geometry.size).to_physical_precise_down(scale);
|
||||
OpaqueRegions::from_slice(&[rect])
|
||||
} else {
|
||||
OpaqueRegions::default()
|
||||
|
||||
@@ -157,7 +157,7 @@ impl<T: Texture> Element for TextureRenderElement<T> {
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
let logical_geo = Rectangle::from_loc_and_size(self.location, self.logical_size());
|
||||
let logical_geo = Rectangle::new(self.location, self.logical_size());
|
||||
logical_geo.to_physical_precise_round(scale)
|
||||
}
|
||||
|
||||
@@ -174,9 +174,7 @@ impl<T: Texture> Element for TextureRenderElement<T> {
|
||||
&self.buffer.logical_size(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Rectangle::from_loc_and_size((0, 0), self.buffer.texture.size()).to_f64()
|
||||
})
|
||||
.unwrap_or_else(|| Rectangle::from_size(self.buffer.texture.size()).to_f64())
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Write as _;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{env, fmt};
|
||||
|
||||
use calloop::EventLoop;
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
use single_pixel_buffer::v1::client::wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1;
|
||||
use smithay::reexports::wayland_protocols::wp::single_pixel_buffer;
|
||||
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
|
||||
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_surface::{self, XdgSurface};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_toplevel::{self, XdgToplevel};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_wm_base::{self, XdgWmBase};
|
||||
use wayland_backend::client::Backend;
|
||||
use wayland_client::globals::Global;
|
||||
use wayland_client::protocol::wl_buffer::{self, WlBuffer};
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_compositor::WlCompositor;
|
||||
use wayland_client::protocol::wl_display::WlDisplay;
|
||||
use wayland_client::protocol::wl_output::{self, WlOutput};
|
||||
use wayland_client::protocol::wl_registry::{self, WlRegistry};
|
||||
use wayland_client::protocol::wl_surface::{self, WlSurface};
|
||||
use wayland_client::{Connection, Dispatch, Proxy as _, QueueHandle};
|
||||
|
||||
use crate::utils::id::IdCounter;
|
||||
|
||||
pub struct Client {
|
||||
pub id: ClientId,
|
||||
pub event_loop: EventLoop<'static, State>,
|
||||
pub connection: Connection,
|
||||
pub qh: QueueHandle<State>,
|
||||
pub display: WlDisplay,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub qh: QueueHandle<State>,
|
||||
|
||||
pub globals: Vec<Global>,
|
||||
pub outputs: HashMap<WlOutput, String>,
|
||||
|
||||
pub compositor: Option<WlCompositor>,
|
||||
pub xdg_wm_base: Option<XdgWmBase>,
|
||||
pub spbm: Option<WpSinglePixelBufferManagerV1>,
|
||||
pub viewporter: Option<WpViewporter>,
|
||||
|
||||
pub windows: Vec<Window>,
|
||||
}
|
||||
|
||||
pub struct Window {
|
||||
pub qh: QueueHandle<State>,
|
||||
pub spbm: WpSinglePixelBufferManagerV1,
|
||||
|
||||
pub surface: WlSurface,
|
||||
pub xdg_surface: XdgSurface,
|
||||
pub xdg_toplevel: XdgToplevel,
|
||||
pub viewport: WpViewport,
|
||||
pub pending_configure: Configure,
|
||||
pub configures_received: Vec<(u32, Configure)>,
|
||||
pub close_requsted: bool,
|
||||
|
||||
pub configures_looked_at: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Configure {
|
||||
pub size: (i32, i32),
|
||||
pub bounds: Option<(i32, i32)>,
|
||||
pub states: Vec<xdg_toplevel::State>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SyncData {
|
||||
pub done: AtomicBool,
|
||||
}
|
||||
|
||||
static CLIENT_ID_COUNTER: IdCounter = IdCounter::new();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ClientId(u64);
|
||||
|
||||
impl ClientId {
|
||||
fn next() -> ClientId {
|
||||
ClientId(CLIENT_ID_COUNTER.next())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Configure {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "size: {} × {}, ", self.size.0, self.size.1)?;
|
||||
if let Some(bounds) = self.bounds {
|
||||
write!(f, "bounds: {} × {}, ", bounds.0, bounds.1)?;
|
||||
} else {
|
||||
write!(f, "bounds: none, ")?;
|
||||
}
|
||||
write!(f, "states: {:?}", self.states)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(socket_name: &OsStr) -> Connection {
|
||||
let mut socket_path = PathBuf::from(env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||
socket_path.push(socket_name);
|
||||
|
||||
let stream = UnixStream::connect(socket_path).unwrap();
|
||||
let backend = Backend::connect(stream).unwrap();
|
||||
Connection::from_backend(backend)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(socket_name: &OsStr) -> Self {
|
||||
let id = ClientId::next();
|
||||
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let connection = connect(socket_name);
|
||||
let queue = connection.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
WaylandSource::new(connection.clone(), queue)
|
||||
.insert(event_loop.handle())
|
||||
.unwrap();
|
||||
|
||||
let display = connection.display();
|
||||
let _registry = display.get_registry(&qh, ());
|
||||
connection.flush().unwrap();
|
||||
|
||||
let state = State {
|
||||
qh: qh.clone(),
|
||||
globals: Vec::new(),
|
||||
outputs: HashMap::new(),
|
||||
compositor: None,
|
||||
xdg_wm_base: None,
|
||||
spbm: None,
|
||||
viewporter: None,
|
||||
windows: Vec::new(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
event_loop,
|
||||
connection,
|
||||
qh,
|
||||
display,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self) {
|
||||
self.event_loop
|
||||
.dispatch(Duration::ZERO, &mut self.state)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn send_sync(&self) -> Arc<SyncData> {
|
||||
let data = Arc::new(SyncData::default());
|
||||
self.display.sync(&self.qh, data.clone());
|
||||
self.connection.flush().unwrap();
|
||||
data
|
||||
}
|
||||
|
||||
pub fn create_window(&mut self) -> &mut Window {
|
||||
self.state.create_window()
|
||||
}
|
||||
|
||||
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
|
||||
self.state.window(surface)
|
||||
}
|
||||
|
||||
pub fn output(&mut self, name: &str) -> WlOutput {
|
||||
self.state
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|(_, v)| *v == name)
|
||||
.unwrap()
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn create_window(&mut self) -> &mut Window {
|
||||
let compositor = self.compositor.as_ref().unwrap();
|
||||
let xdg_wm_base = self.xdg_wm_base.as_ref().unwrap();
|
||||
let viewporter = self.viewporter.as_ref().unwrap();
|
||||
|
||||
let surface = compositor.create_surface(&self.qh, ());
|
||||
let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &self.qh, ());
|
||||
let xdg_toplevel = xdg_surface.get_toplevel(&self.qh, ());
|
||||
let viewport = viewporter.get_viewport(&surface, &self.qh, ());
|
||||
|
||||
let window = Window {
|
||||
qh: self.qh.clone(),
|
||||
spbm: self.spbm.clone().unwrap(),
|
||||
|
||||
surface,
|
||||
xdg_surface,
|
||||
xdg_toplevel,
|
||||
viewport,
|
||||
pending_configure: Configure::default(),
|
||||
configures_received: Vec::new(),
|
||||
close_requsted: false,
|
||||
|
||||
configures_looked_at: 0,
|
||||
};
|
||||
|
||||
self.windows.push(window);
|
||||
self.windows.last_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
|
||||
self.windows
|
||||
.iter_mut()
|
||||
.find(|w| w.surface == *surface)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn commit(&self) {
|
||||
self.surface.commit();
|
||||
}
|
||||
|
||||
pub fn ack_last(&self) {
|
||||
let serial = self.configures_received.last().unwrap().0;
|
||||
self.xdg_surface.ack_configure(serial);
|
||||
}
|
||||
|
||||
pub fn ack_last_and_commit(&self) {
|
||||
self.ack_last();
|
||||
self.commit();
|
||||
}
|
||||
|
||||
pub fn attach_new_buffer(&self) {
|
||||
let buffer = self.spbm.create_u32_rgba_buffer(0, 0, 0, 0, &self.qh, ());
|
||||
self.surface.attach(Some(&buffer), 0, 0);
|
||||
}
|
||||
|
||||
pub fn set_size(&self, w: u16, h: u16) {
|
||||
self.viewport.set_destination(i32::from(w), i32::from(h));
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&self, output: Option<&WlOutput>) {
|
||||
self.xdg_toplevel.set_fullscreen(output);
|
||||
}
|
||||
|
||||
pub fn unset_fullscreen(&self) {
|
||||
self.xdg_toplevel.unset_fullscreen();
|
||||
}
|
||||
|
||||
pub fn set_parent(&self, parent: Option<&XdgToplevel>) {
|
||||
self.xdg_toplevel.set_parent(parent);
|
||||
}
|
||||
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.xdg_toplevel.set_title(title.to_owned());
|
||||
}
|
||||
|
||||
pub fn recent_configures(&mut self) -> impl Iterator<Item = &Configure> {
|
||||
let start = self.configures_looked_at;
|
||||
self.configures_looked_at = self.configures_received.len();
|
||||
self.configures_received[start..].iter().map(|(_, c)| c)
|
||||
}
|
||||
|
||||
pub fn format_recent_configures(&mut self) -> String {
|
||||
let mut buf = String::new();
|
||||
for configure in self.recent_configures() {
|
||||
if !buf.is_empty() {
|
||||
buf.push('\n');
|
||||
}
|
||||
write!(buf, "{configure}").unwrap();
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlCallback, Arc<SyncData>> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlCallback,
|
||||
event: <WlCallback as wayland_client::Proxy>::Event,
|
||||
data: &Arc<SyncData>,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_callback::Event::Done { .. } => data.done.store(true, Ordering::Relaxed),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &WlRegistry,
|
||||
event: <WlRegistry as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} => {
|
||||
if interface == WlCompositor::interface().name {
|
||||
let version = min(version, WlCompositor::interface().version);
|
||||
state.compositor = Some(registry.bind(name, version, qh, ()));
|
||||
} else if interface == XdgWmBase::interface().name {
|
||||
let version = min(version, XdgWmBase::interface().version);
|
||||
state.xdg_wm_base = Some(registry.bind(name, version, qh, ()));
|
||||
} else if interface == WpSinglePixelBufferManagerV1::interface().name {
|
||||
let version = min(version, WpSinglePixelBufferManagerV1::interface().version);
|
||||
state.spbm = Some(registry.bind(name, version, qh, ()));
|
||||
} else if interface == WpViewporter::interface().name {
|
||||
let version = min(version, WpViewporter::interface().version);
|
||||
state.viewporter = Some(registry.bind(name, version, qh, ()));
|
||||
} else if interface == WlOutput::interface().name {
|
||||
let version = min(version, WlOutput::interface().version);
|
||||
let output = registry.bind(name, version, qh, ());
|
||||
state.outputs.insert(output, String::new());
|
||||
}
|
||||
|
||||
let global = Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
};
|
||||
state.globals.push(global);
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { .. } => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
output: &WlOutput,
|
||||
event: <WlOutput as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_output::Event::Geometry { .. } => (),
|
||||
wl_output::Event::Mode { .. } => (),
|
||||
wl_output::Event::Done => (),
|
||||
wl_output::Event::Scale { .. } => (),
|
||||
wl_output::Event::Name { name } => {
|
||||
*state.outputs.get_mut(output).unwrap() = name;
|
||||
}
|
||||
wl_output::Event::Description { .. } => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlCompositor, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlCompositor,
|
||||
_event: <WlCompositor as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgWmBase, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
xdg_wm_base: &XdgWmBase,
|
||||
event: <XdgWmBase as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
xdg_wm_base::Event::Ping { serial } => {
|
||||
xdg_wm_base.pong(serial);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlSurface, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlSurface,
|
||||
event: <WlSurface as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_surface::Event::Enter { .. } => (),
|
||||
wl_surface::Event::Leave { .. } => (),
|
||||
wl_surface::Event::PreferredBufferScale { .. } => (),
|
||||
wl_surface::Event::PreferredBufferTransform { .. } => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgSurface, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
xdg_surface: &XdgSurface,
|
||||
event: <XdgSurface as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
xdg_surface::Event::Configure { serial } => {
|
||||
let window = state
|
||||
.windows
|
||||
.iter_mut()
|
||||
.find(|w| w.xdg_surface == *xdg_surface)
|
||||
.unwrap();
|
||||
let configure = window.pending_configure.clone();
|
||||
window.configures_received.push((serial, configure));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgToplevel, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
xdg_toplevel: &XdgToplevel,
|
||||
event: <XdgToplevel as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
let window = state
|
||||
.windows
|
||||
.iter_mut()
|
||||
.find(|w| w.xdg_toplevel == *xdg_toplevel)
|
||||
.unwrap();
|
||||
|
||||
match event {
|
||||
xdg_toplevel::Event::Configure {
|
||||
width,
|
||||
height,
|
||||
states,
|
||||
} => {
|
||||
let configure = &mut window.pending_configure;
|
||||
configure.size = (width, height);
|
||||
configure.states = states
|
||||
.chunks_exact(4)
|
||||
.flat_map(TryInto::<[u8; 4]>::try_into)
|
||||
.map(u32::from_ne_bytes)
|
||||
.flat_map(xdg_toplevel::State::try_from)
|
||||
.collect();
|
||||
}
|
||||
xdg_toplevel::Event::Close => {
|
||||
window.close_requsted = true;
|
||||
}
|
||||
xdg_toplevel::Event::ConfigureBounds { width, height } => {
|
||||
window.pending_configure.bounds = Some((width, height));
|
||||
}
|
||||
xdg_toplevel::Event::WmCapabilities { .. } => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlBuffer, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlBuffer,
|
||||
event: <WlBuffer as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_buffer::Event::Release => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WpSinglePixelBufferManagerV1, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WpSinglePixelBufferManagerV1,
|
||||
_event: <WpSinglePixelBufferManagerV1 as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WpViewporter, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WpViewporter,
|
||||
_event: <WpViewporter as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WpViewport, ()> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WpViewport,
|
||||
_event: <WpViewport as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use std::os::fd::AsFd as _;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use calloop::generic::Generic;
|
||||
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
|
||||
use niri_config::Config;
|
||||
use smithay::output::Output;
|
||||
|
||||
use super::client::{Client, ClientId};
|
||||
use super::server::Server;
|
||||
use crate::niri::Niri;
|
||||
|
||||
pub struct Fixture {
|
||||
pub event_loop: EventLoop<'static, State>,
|
||||
pub handle: LoopHandle<'static, State>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub server: Server,
|
||||
pub clients: Vec<Client>,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn new() -> Self {
|
||||
Self::with_config(Config::default())
|
||||
}
|
||||
|
||||
pub fn with_config(config: Config) -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let handle = event_loop.handle();
|
||||
|
||||
let server = Server::new(config);
|
||||
let fd = server.event_loop.as_fd().try_clone_to_owned().unwrap();
|
||||
let source = Generic::new(fd, Interest::READ, Mode::Level);
|
||||
handle
|
||||
.insert_source(source, |_, _, state: &mut State| {
|
||||
state.server.dispatch();
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let state = State {
|
||||
server,
|
||||
clients: Vec::new(),
|
||||
};
|
||||
|
||||
Self {
|
||||
event_loop,
|
||||
handle,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self) {
|
||||
self.event_loop
|
||||
.dispatch(Duration::ZERO, &mut self.state)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn niri_state(&mut self) -> &mut crate::niri::State {
|
||||
&mut self.state.server.state
|
||||
}
|
||||
|
||||
pub fn niri(&mut self) -> &mut Niri {
|
||||
&mut self.niri_state().niri
|
||||
}
|
||||
|
||||
pub fn niri_output(&self, n: u8) -> Output {
|
||||
let niri = &self.state.server.state.niri;
|
||||
let idx = usize::from(n - 1);
|
||||
let output = niri.global_space.outputs().nth(idx).unwrap();
|
||||
output.clone()
|
||||
}
|
||||
|
||||
pub fn niri_focus_output(&mut self, n: u8) {
|
||||
let niri = &mut self.state.server.state.niri;
|
||||
let idx = usize::from(n - 1);
|
||||
let output = niri.global_space.outputs().nth(idx).unwrap();
|
||||
niri.layout.focus_output(output);
|
||||
}
|
||||
|
||||
pub fn add_output(&mut self, n: u8, size: (u16, u16)) {
|
||||
let state = self.niri_state();
|
||||
let niri = &mut state.niri;
|
||||
state.backend.headless().add_output(niri, n, size);
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self) -> ClientId {
|
||||
let client = Client::new(&self.state.server.state.niri.socket_name);
|
||||
let id = client.id;
|
||||
|
||||
let fd = client.event_loop.as_fd().try_clone_to_owned().unwrap();
|
||||
let source = Generic::new(fd, Interest::READ, Mode::Level);
|
||||
self.handle
|
||||
.insert_source(source, move |_, _, state: &mut State| {
|
||||
state.client(id).dispatch();
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.state.clients.push(client);
|
||||
self.roundtrip(id);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn client(&mut self, id: ClientId) -> &mut Client {
|
||||
self.state.client(id)
|
||||
}
|
||||
|
||||
pub fn roundtrip(&mut self, id: ClientId) {
|
||||
let client = self.state.client(id);
|
||||
let data = client.send_sync();
|
||||
while !data.done.load(Ordering::Relaxed) {
|
||||
self.dispatch();
|
||||
}
|
||||
}
|
||||
|
||||
/// Rountrip twice in a row.
|
||||
///
|
||||
/// For some reason, when running tests on many threads at once, a single roundtrip is
|
||||
/// sometimes not sufficient to get the configure events to the client.
|
||||
///
|
||||
/// I suspect that this is because these configure events are sent from the niri loop callback,
|
||||
/// so they arrive after the sync done event and don't get processed in that client dispatch
|
||||
/// cycle. I'm not sure why this would be dependent on multithreading. But if this is indeed
|
||||
/// the issue, then a double roundtrip fixes it.
|
||||
pub fn double_roundtrip(&mut self, id: ClientId) {
|
||||
self.roundtrip(id);
|
||||
self.roundtrip(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn client(&mut self, id: ClientId) -> &mut Client {
|
||||
self.clients.iter_mut().find(|c| c.id == id).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,856 @@
|
||||
use client::ClientId;
|
||||
use insta::assert_snapshot;
|
||||
use niri_config::Config;
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::utils::Point;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Sets up a fixture with two outputs and 100×100 window.
|
||||
fn set_up() -> (Fixture, ClientId, WlSurface) {
|
||||
let mut f = Fixture::new();
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
let id = f.add_client();
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(100, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
(f, id, surface)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfocus_preserves_current_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.roundtrip(id);
|
||||
|
||||
// Change window size while it's floating.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Focus a different output which should drop the Activated state.
|
||||
f.niri_focus_output(2);
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request 200 × 200 because that's the current window size.
|
||||
let window = f.client(id).window(&surface);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Change window size again.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(300, 300);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Focus the first output which should add back the Activated state.
|
||||
f.niri_focus_output(1);
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request 300 × 300 because that's the current window size.
|
||||
let window = f.client(id).window(&surface);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_to_different_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Commit in response to the Activated state change configure.
|
||||
f.client(id).window(&surface).ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size, 500 × 100.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Focus a different output which should drop the Activated state.
|
||||
f.niri_focus_output(2);
|
||||
f.double_roundtrip(id);
|
||||
// This should request the new size since the window hasn't committed yet.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Ack but don't commit yet.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.ack_last();
|
||||
f.roundtrip(id);
|
||||
// Add the activated state.
|
||||
f.niri_focus_output(1);
|
||||
f.double_roundtrip(id);
|
||||
// This should request the new size since the window hasn't committed yet.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Commit but with some different size.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.commit();
|
||||
f.double_roundtrip(id);
|
||||
// This shouldn't request anything.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
|
||||
// Drop the Activated state.
|
||||
f.niri_focus_output(2);
|
||||
f.double_roundtrip(id);
|
||||
// This should request the current window size rather than keep requesting 500 × 100.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_window_width_uses_current_height() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Resize to something different on both axes.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Request a width change.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should use the current window height (200), rather than the initial window height (100).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_window_height_uses_current_width() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Resize to something different on both axes.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Request a width change.
|
||||
f.niri()
|
||||
.layout
|
||||
.set_window_height(None, SizeChange::SetFixed(500));
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should use the current window width (200), rather than the initial window width (100).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 500, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_to_same_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Resize to something different.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Request a size change to the same size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(200));
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This needn't request anything because we're already that size; the size in the current
|
||||
// server state matches the requested size.
|
||||
//
|
||||
// FIXME: However, currently it will request the size anyway because the code checks the
|
||||
// current server state, and the last size niri requested of the window was 100×100 (even if
|
||||
// the window already acked and committed in response).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_to_different_then_same() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Commit in response to any configure from the floating change.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Request a size change to a different size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Before the window has a chance to respond, request a size change to the same, new size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
|
||||
// And also drop the Activated state to have some pending change.
|
||||
f.niri_focus_output(2);
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should keep requesting the new size (500 × 100) since the window has not responded to
|
||||
// it yet.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Commit in response to the size change request.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(300, 300);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// And also add the Activated state to have some pending change.
|
||||
f.niri_focus_output(1);
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the current window size (300 × 300) since the window has committed in
|
||||
// response to the size change.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_floating_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Change size while we're floating and commit in response to the floating configure.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Change back to tiling.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// We should get a tiling size configure.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
|
||||
// Resize as requested.
|
||||
let window = f.client(id).window(&surface);
|
||||
let (_, configure) = window.configures_received.last().unwrap();
|
||||
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Change back to floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// We should get a configure restoring out previous 200 × 200 size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_across_workspaces_doesnt_cancel_resize() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Change size while we're floating and commit in response to the floating configure.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Request a size change to a different size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Move to a different workspace before the window has a chance to respond. This will remove it
|
||||
// from one floating layout and add into a different one, potentially causing a size request.
|
||||
f.niri().layout.move_to_workspace_down();
|
||||
// Drop the Activated state to force a configure.
|
||||
f.niri_focus_output(2);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size again (500 × 200) since the window hasn't responded to it.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 200, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Respond to the resize with a different size.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(300, 300);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Focus, adding Activated, and move to workspace down, causing removing and adding to a
|
||||
// floating layout.
|
||||
f.niri_focus_output(1);
|
||||
f.niri().layout.move_to_workspace_down();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the current size (300 × 300) since the window responded to the change.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_to_floating_doesnt_cancel_resize() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Request a size change to a different size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size (500 ×).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 1048, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
|
||||
// Before the window has a chance to respond, make it floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should keep requesting the new size (500 ×).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 1048, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_move_unfullscreen_to_floating_restores_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Change size while we're floating and commit.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window = mapped.window.clone();
|
||||
niri.layout.set_fullscreen(&window, true);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request a fullscreen size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 1920 × 1080, bounds: 1888 × 1048, states: [Activated, Fullscreen]"
|
||||
);
|
||||
|
||||
// Start an interactive move which causes an unfullscreen into floating.
|
||||
let output = f.niri_output(1);
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window = mapped.window.clone();
|
||||
niri.layout
|
||||
.interactive_move_begin(window.clone(), &output, Point::default());
|
||||
niri.layout.interactive_move_update(
|
||||
&window,
|
||||
Point::from((1000., 0.)),
|
||||
output,
|
||||
Point::default(),
|
||||
);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the stored floating size (200 × 200).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_during_interactive_move_propagates_to_floating() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Change size while we're floating and commit.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Start an interactive move.
|
||||
let output = f.niri_output(1);
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window_id = mapped.window.clone();
|
||||
niri.layout
|
||||
.interactive_move_begin(window_id.clone(), &output, Point::default());
|
||||
niri.layout.interactive_move_update(
|
||||
&window_id,
|
||||
Point::from((1000., 0.)),
|
||||
output,
|
||||
Point::default(),
|
||||
);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request any new size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
|
||||
// Change size while we're being interactively moved.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(300, 300);
|
||||
window.commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request any new size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
|
||||
// End the interactive move, placing the window into floating.
|
||||
f.niri().layout.interactive_move_end(&window_id);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should keep the new 300 × 300 size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_in_steps() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Commit in response to the floating bounds state change configure.
|
||||
f.client(id).window(&surface).ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Request a size change to a different size in two steps.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
f.niri()
|
||||
.layout
|
||||
.set_window_height(None, SizeChange::SetFixed(500));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the full new size (500 × 500) once.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 500, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
let serial = window.configures_received.last().unwrap().0;
|
||||
|
||||
// Request a size change now that the previous one is pending-but-not-acked.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(600));
|
||||
// Drop Activated to work around resize throttling.
|
||||
f.niri_focus_output(2);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size (600 × 500) once.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 600 × 500, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Commit in response to the previous configure.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.xdg_surface.ack_configure(serial);
|
||||
window.set_size(500, 500);
|
||||
window.commit();
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request anything.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
|
||||
// Request a height change now that the first one is committed-to, but the second isn't.
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window = mapped.window.clone();
|
||||
f.niri()
|
||||
.layout
|
||||
.set_window_height(Some(&window), SizeChange::SetFixed(600));
|
||||
// Add Activated to work around resize throttling.
|
||||
f.niri_focus_output(1);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the latest sizes (600 × 600).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 600 × 600, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_change_doesnt_break_use_window_size() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Commit in response to the bounds change that comes with toggling floating.
|
||||
f.client(id).window(&surface).ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Request a size change to a different size.
|
||||
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size (500 × 100).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
let serial = window.configures_received.last().unwrap().0;
|
||||
|
||||
// Request a state change by dropping Activated.
|
||||
f.niri_focus_output(2);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the new size (500 × 100).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
|
||||
);
|
||||
|
||||
// Commit in response to the previous configure with a different size.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.xdg_surface.ack_configure(serial);
|
||||
window.set_size(300, 300);
|
||||
window.commit();
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request anything.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
|
||||
// Request a height change now that the first one is committed-to, but the second isn't.
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window = mapped.window.clone();
|
||||
f.niri()
|
||||
.layout
|
||||
.set_window_height(Some(&window), SizeChange::SetFixed(600));
|
||||
// Add Activated state to force a configure.
|
||||
f.niri_focus_output(1);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should already request the current width (300 × 600) rather than the pending previous
|
||||
// width (500 × 600) from the state change configure.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 300 × 600, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_move_restores_floating_size_when_set_to_floating() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Change size while we're floating and commit to make niri remember it.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(200, 200);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Change back to tiling.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// We should get a tiled size configure.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
|
||||
// Resize as requested.
|
||||
let window = f.client(id).window(&surface);
|
||||
let (_, configure) = window.configures_received.last().unwrap();
|
||||
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Start an interactive move.
|
||||
let output = f.niri_output(1);
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window_id = mapped.window.clone();
|
||||
niri.layout
|
||||
.interactive_move_begin(window_id.clone(), &output, Point::default());
|
||||
niri.layout.interactive_move_update(
|
||||
&window_id,
|
||||
Point::from((1000., 0.)),
|
||||
output,
|
||||
Point::default(),
|
||||
);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request any new size because interactive move targets tiling.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 1048, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Change interactive move to target floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should restore the floating window size (200 × 200).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// End the interactive move, placing the window into floating.
|
||||
f.niri().layout.interactive_move_end(&window_id);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should keep the floating window size (200 × 200).
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_doesnt_store_fullscreen_size() {
|
||||
let mut f = Fixture::new();
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
// Open a window fullscreen.
|
||||
let id = f.add_client();
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.set_fullscreen(None);
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(1920, 1080);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Make it floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request 0 × 0 to unfullscreen.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 0 × 0, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Without committing, make it tiling again. We never committed while floating, so there's no
|
||||
// floating size to remember.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should request the tiled size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 1920 × 1048, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
|
||||
// Commit in response.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(100, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Make the window floating again.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This shouldn't request any size change, particularly not the fullscreen size.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 100 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_respects_non_fixed_min_max_rule() {
|
||||
let config = r##"
|
||||
window-rule {
|
||||
min-width 200
|
||||
max-width 300
|
||||
}
|
||||
"##;
|
||||
let config = Config::parse("test.kdl", config).unwrap();
|
||||
let mut f = Fixture::with_config(config);
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
let id = f.add_client();
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Open with smaller width than min.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(100, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Commit to the Activated state configure.
|
||||
f.client(id).window(&surface).ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Make it floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should clamp to min-width and request 200 × 100.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 200 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
|
||||
// Commit with a bigger width than max.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_size(400, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Make it tiling.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
f.client(id).window(&surface).ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
// Make it floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should clamp to max-width and request 300 × 100.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 300 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use fixture::Fixture;
|
||||
|
||||
mod client;
|
||||
mod fixture;
|
||||
mod server;
|
||||
|
||||
mod floating;
|
||||
mod window_opening;
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use calloop::EventLoop;
|
||||
use niri_config::Config;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct Server {
|
||||
pub event_loop: EventLoop<'static, State>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(config: Config) -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let handle = event_loop.handle();
|
||||
let display = Display::new().unwrap();
|
||||
let state = State::new(
|
||||
config,
|
||||
handle.clone(),
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Self { event_loop, state }
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self) {
|
||||
self.event_loop
|
||||
.dispatch(Duration::ZERO, &mut self.state)
|
||||
.unwrap();
|
||||
self.state.refresh_and_flush_clients();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user