mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
223 Commits
v0.1.0-beta.1
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| acd33653b3 | |||
| f7c6516da7 | |||
| b220420fba | |||
| bbeaba16a0 | |||
| 9d7c39b89a | |||
| 03fe864d07 | |||
| e45dbb8ef6 | |||
| 5c4b71a5a4 | |||
| 348690afb6 | |||
| ca22e70cc4 | |||
| 1a784e6e66 | |||
| 3ee2db71a4 | |||
| cedfd4944c | |||
| 431f070481 | |||
| 9cbbffc23c | |||
| c6a1398d51 | |||
| f9127616b0 | |||
| ae89b2e514 | |||
| 732f7f6f33 | |||
| 8bebd54c6d | |||
| 1978e5b0b8 | |||
| 60b02545f3 | |||
| 2750b2038b | |||
| c4145b014a | |||
| 2e51efd3a3 | |||
| caea05433e | |||
| e4f78c26f0 | |||
| 1548db56ce | |||
| 5f416abcf9 | |||
| 66c1272420 | |||
| e0ec6e5b11 | |||
| 93243d7772 | |||
| 24537ec2ba | |||
| 88ac16c99a | |||
| 0add457cf0 | |||
| 6e5426ef22 | |||
| 202406aadf | |||
| 92d9c7ff4f | |||
| 28977d1d3f | |||
| ba10bab010 | |||
| 55038b7c07 | |||
| 8018839f5d | |||
| 077f22edd6 | |||
| 4f7c3300ef | |||
| 5628bf7d77 | |||
| 719697179f | |||
| 5ac350d51c | |||
| 494e98c123 | |||
| ec156a8587 | |||
| e278e871c3 | |||
| ab9d1aab4e | |||
| 506dcd99d7 | |||
| dfbc024127 | |||
| eb2dce1b53 | |||
| f5b776a947 | |||
| 6a587245eb | |||
| 2317021a7c | |||
| af6485cd8c | |||
| f32a25eefe | |||
| aefbad0cf7 | |||
| b091202d86 | |||
| 48f0f6fb3c | |||
| 340bac0690 | |||
| d1b8134337 | |||
| 646e3d8995 | |||
| d1fe6930a7 | |||
| 9e60b344d0 | |||
| 2c01cde9be | |||
| cb9dc9c0cd | |||
| 73d2807b4b | |||
| 7d41f113cb | |||
| 63e5cf8798 | |||
| 9ce19ad7de | |||
| 751f79dc35 | |||
| b8aa0a86e7 | |||
| 82fffdea80 | |||
| 5b3bfd95d9 | |||
| 1a15aa704d | |||
| d58a45a96c | |||
| 9f1b4ee299 | |||
| f0a5e9c933 | |||
| c4c07841d7 | |||
| 6ba24e341f | |||
| 13b6c74cc3 | |||
| d8fb8d5ef0 | |||
| 2b5eeb6162 | |||
| 85be5f746c | |||
| dd7362913e | |||
| 62892d6361 | |||
| 31c13b6a69 | |||
| baaac2f3c4 | |||
| 3fdefae45b | |||
| 6345224e95 | |||
| b3d2096439 | |||
| 94ded2f6a9 | |||
| fa3bc69f94 | |||
| 363e1d8764 | |||
| 8e1d4de0dc | |||
| 72e3fadb9a | |||
| 78cda2e67f | |||
| 924e21f69b | |||
| befdebfa03 | |||
| 7960a73e9d | |||
| 749ee5d627 | |||
| 952dd48115 | |||
| cbd066ab68 | |||
| bccde351fb | |||
| beaffb1b97 | |||
| 385454378b | |||
| 18f06a7acd | |||
| 6e23073019 | |||
| a9fcbf81eb | |||
| a99f34cba8 | |||
| bd2277fa25 | |||
| 67182129ff | |||
| d6b116d229 | |||
| c20a843ab2 | |||
| 1b752fe08f | |||
| 89f74aae98 | |||
| 5e553c2679 | |||
| cabf712821 | |||
| 0931447ec1 | |||
| a388c25795 | |||
| 5c4d9824a4 | |||
| ca4ee5ae25 | |||
| 93e16a6582 | |||
| 3486fa5536 | |||
| c022d74c82 | |||
| e68641c0a7 | |||
| 2a892ef511 | |||
| 90c6721e97 | |||
| e5cd9e9307 | |||
| 573dca10cc | |||
| 577fba82e5 | |||
| b9116c579a | |||
| d8dcadc5b2 | |||
| 6424a2738d | |||
| 753a90430a | |||
| f9085db564 | |||
| 49ce791d13 | |||
| 4b8e04da04 | |||
| 026ad8f377 | |||
| 0761401650 | |||
| 3360517f62 | |||
| 9896fd67a0 | |||
| 15ec699fbb | |||
| a1cc39a437 | |||
| 738d9a2b40 | |||
| 68752db51b | |||
| d4929b8e18 | |||
| 93c547f749 | |||
| e2b91c0c1c | |||
| 322b5cbac7 | |||
| 592791611a | |||
| d073d2ab3d | |||
| b2298db5c5 | |||
| baa6263cbe | |||
| 795da53d53 | |||
| 122afff7d1 | |||
| d2a4e6a0cb | |||
| 8916b18c6b | |||
| b0d0fce5f3 | |||
| 3dc4a5fdac | |||
| 1706a46b2b | |||
| 3789d85588 | |||
| 3a23417e98 | |||
| 6bb83757ee | |||
| b62a07956a | |||
| 96016790b2 | |||
| bf978fe98d | |||
| 57521c69c3 | |||
| da826e42aa | |||
| b824cf90ab | |||
| 7a4bb8ba8a | |||
| 72c8f569ac | |||
| 798d9c55df | |||
| 05613eed1e | |||
| b23dd4b800 | |||
| 1f72089a46 | |||
| fbe9020915 | |||
| 2036116f16 | |||
| 9afd728ae9 | |||
| e51268a39e | |||
| 0a715ce155 | |||
| 89ac958670 | |||
| 2e50f8dee0 | |||
| 7052f0129e | |||
| 962e159db6 | |||
| 11bff3a2f1 | |||
| 15606304f2 | |||
| 85eac9d9d0 | |||
| d3f4583c90 | |||
| fefb1cccd6 | |||
| deef52519a | |||
| 59ff331597 | |||
| b813f99abd | |||
| d9b9cec8b8 | |||
| 597ea62d17 | |||
| 51243a0a50 | |||
| 0ebcc3e0d6 | |||
| 64c85d865e | |||
| 367e4955ea | |||
| dd967554d1 | |||
| 6d7c220137 | |||
| d77aac1afa | |||
| 837a0a20fb | |||
| ecdf756b55 | |||
| 73f3c160b2 | |||
| 5f99eb13ab | |||
| 20326b093c | |||
| 467d92a4b4 | |||
| 15bb69c0b9 | |||
| adfbfdffb3 | |||
| 087ed260c5 | |||
| f5642ab733 | |||
| ab9706cb30 | |||
| 05f2a3709b | |||
| 743173ef64 | |||
| cbbb7a26fc | |||
| 18566e3366 | |||
| df48337d83 | |||
| f5e9b40140 | |||
| 5cacd03e85 |
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug or a crash
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||
|
||||
### System Information
|
||||
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
|
||||
* GPU:
|
||||
|
||||
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
|
||||
* CPU:
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||
about: Ideas for new features and functionality (start a Discussion)
|
||||
+94
-24
@@ -24,6 +24,7 @@ jobs:
|
||||
|
||||
name: test - ${{ matrix.configuration }}
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -32,34 +33,90 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
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
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.configuration }}
|
||||
|
||||
- name: Build (no default features)
|
||||
run: cargo build ${{ matrix.release-flag }} --no-default-features
|
||||
- name: Check (no default features)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features
|
||||
|
||||
- name: Build
|
||||
run: cargo build ${{ matrix.release-flag }}
|
||||
- name: Check (just dbus)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
|
||||
|
||||
- name: Check (just systemd)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
|
||||
|
||||
- name: Check (just dinit)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
|
||||
|
||||
- name: Check (just xdp-gnome-screencast)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
|
||||
|
||||
- name: Check
|
||||
run: cargo check ${{ matrix.release-flag }}
|
||||
|
||||
- name: Build (with profiling)
|
||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
||||
|
||||
- name: Build Tests
|
||||
run: cargo test --no-run --all ${{ matrix.release-flag }}
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
|
||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
visual-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --package niri-visual-tests
|
||||
|
||||
msrv:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.72.0'
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.72.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- run: cargo check --all-targets
|
||||
|
||||
clippy:
|
||||
strategy:
|
||||
@@ -67,6 +124,7 @@ jobs:
|
||||
|
||||
name: clippy
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -75,15 +133,12 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
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
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal --component clippy
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -119,8 +174,23 @@ 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
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build
|
||||
- run: cargo build --all
|
||||
|
||||
nix:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v4
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
continue-on-error: true
|
||||
|
||||
Generated
+763
-338
File diff suppressed because it is too large
Load Diff
+37
-24
@@ -1,5 +1,8 @@
|
||||
[workspace]
|
||||
members = ["niri-visual-tests"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0-beta.1"
|
||||
version = "0.1.3"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -7,11 +10,13 @@ edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.80"
|
||||
bitflags = "2.4.2"
|
||||
directories = "5.0.1"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
clap = { version = "~4.4.18", features = ["derive"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracy-client = { version = "0.16.5", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.17.0", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
@@ -35,38 +40,39 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.79" }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.4"
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
async-channel = { version = "2.2.0", optional = true }
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
bitflags = "2.4.2"
|
||||
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
|
||||
clap = { version = "4.4.18", features = ["derive", "string"] }
|
||||
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.25.0"
|
||||
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.152"
|
||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
logind-zbus = { version = "3.1.2", optional = true }
|
||||
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
|
||||
libc = "0.2.153"
|
||||
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.3", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.3", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "4.10.0", optional = true }
|
||||
pangocairo = "0.18.0"
|
||||
pipewire = { version = "0.7.2", optional = true }
|
||||
png = "0.17.11"
|
||||
pangocairo = "0.19.2"
|
||||
pipewire = { version = "0.8.0", optional = true }
|
||||
png = "0.17.13"
|
||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.13"
|
||||
profiling = "1.0.15"
|
||||
sd-notify = "0.4.1"
|
||||
serde.workspace = true
|
||||
serde_json = "1.0.111"
|
||||
serde_json = "1.0.114"
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.0", optional = true }
|
||||
xcursor = "0.3.5"
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
zbus = { version = "~3.15.2", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -80,6 +86,7 @@ features = [
|
||||
"backend_winit",
|
||||
"desktop",
|
||||
"renderer_gl",
|
||||
"renderer_pixman",
|
||||
"renderer_multi",
|
||||
"use_system_lib",
|
||||
"wayland_frontend",
|
||||
@@ -88,15 +95,20 @@ features = [
|
||||
[dev-dependencies]
|
||||
proptest = "1.4.0"
|
||||
proptest-derive = "0.4.0"
|
||||
xshell = "0.2.5"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "xdp-gnome-screencast"]
|
||||
# Enables DBus support (required for xdp-gnome and power button inhibiting).
|
||||
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
# Enables the Tracy profiler instrumentation.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
# Enables dinit integration (global environment).
|
||||
dinit = []
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -108,7 +120,7 @@ lto = "thin"
|
||||
debug = false
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.0~beta.1"
|
||||
version = "0.1.3"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
@@ -119,3 +131,4 @@ assets = [
|
||||
]
|
||||
[package.metadata.generate-rpm.requires]
|
||||
alacritty = "*"
|
||||
fuzzel = "*"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -16,26 +16,33 @@ Opening a new window never causes existing windows to resize.
|
||||
Every monitor has its own separate window strip.
|
||||
Windows can never "overflow" onto an adjacent monitor.
|
||||
|
||||
Since windows go left-to-right horizontally, workspaces are arranged vertically.
|
||||
Workspaces are dynamic and arranged vertically.
|
||||
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
||||
|
||||
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
|
||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||
|
||||
## Features
|
||||
|
||||
- Scrollable tiling
|
||||
- Dynamic workspaces like in GNOME
|
||||
- Built-in screenshot UI
|
||||
- Monitor screencasting through xdg-desktop-portal-gnome
|
||||
- Touchpad gesture to switch workspaces
|
||||
- Touchpad gestures
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- Live-reloading config
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
|
||||
## Status
|
||||
|
||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||
Feel free to give niri a try.
|
||||
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
Note that NVIDIA GPUs might have rendering issues.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -44,25 +51,25 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
|
||||
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
||||
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||
|
||||
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
|
||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||
## Packages
|
||||
|
||||
There are several community-maintained distribution packages that you can use to install niri.
|
||||
Here are some of them:
|
||||
|
||||
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
|
||||
- NixOS Flake: https://github.com/sodiboo/niri-flake
|
||||
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
|
||||
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
|
||||
|
||||
## Building
|
||||
|
||||
> [!TIP]
|
||||
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||
>
|
||||
> For NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||
|
||||
First, install the dependencies for your distribution.
|
||||
|
||||
- Ubuntu:
|
||||
- Ubuntu 23.10:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
```
|
||||
|
||||
- Fedora:
|
||||
@@ -71,7 +78,9 @@ First, install the dependencies for your distribution.
|
||||
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
|
||||
```
|
||||
|
||||
Next, build niri with `cargo build --release`.
|
||||
Next, get latest stable Rust: https://rustup.rs/
|
||||
|
||||
Then, build niri with `cargo build --release`.
|
||||
|
||||
### NixOS/Nix
|
||||
|
||||
@@ -184,7 +193,6 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
||||
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
|
||||
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
|
||||
|
||||
## Configuration
|
||||
@@ -202,4 +210,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[mako]: https://github.com/emersion/mako
|
||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
|
||||
Generated
+18
-18
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1702918879,
|
||||
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
|
||||
"lastModified": 1709610799,
|
||||
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
|
||||
"rev": "81c393c776d5379c030607866afef6406ca1be57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701411808,
|
||||
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
|
||||
"lastModified": 1709274179,
|
||||
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
|
||||
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,11 +47,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -62,11 +62,11 @@
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1701697642,
|
||||
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
|
||||
"lastModified": 1705332318,
|
||||
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
|
||||
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,11 +77,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1702900294,
|
||||
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
|
||||
"lastModified": 1709386671,
|
||||
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
|
||||
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -103,11 +103,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1701372675,
|
||||
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
|
||||
"lastModified": 1709219524,
|
||||
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
|
||||
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -39,22 +39,22 @@
|
||||
pname = "niri";
|
||||
version = self.rev or "dirty";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ./.;
|
||||
include = [
|
||||
./src
|
||||
./niri-config
|
||||
./niri-ipc
|
||||
./Cargo.toml
|
||||
./Cargo.lock
|
||||
./resources
|
||||
];
|
||||
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));
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
autoPatchelfHook
|
||||
clang
|
||||
gdk-pixbuf
|
||||
graphene
|
||||
gtk4
|
||||
libadwaita
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
|
||||
@@ -9,8 +9,11 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.6.2"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
smithay.workspace = true
|
||||
niri-ipc = { version = "0.1.3", path = "../niri-ipc" }
|
||||
regex = "1.10.3"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
+1078
-138
File diff suppressed because it is too large
Load Diff
@@ -8,4 +8,8 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
|
||||
+260
-3
@@ -2,6 +2,7 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -9,21 +10,225 @@ use serde::{Deserialize, Serialize};
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
|
||||
/// Request from client to niri.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Request {
|
||||
/// Request information about connected outputs.
|
||||
Outputs,
|
||||
/// Perform an action.
|
||||
Action(Action),
|
||||
}
|
||||
|
||||
/// Response from niri to client.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
/// Reply from niri to client.
|
||||
///
|
||||
/// Every request gets one reply.
|
||||
///
|
||||
/// * If an error had occurred, it will be an `Reply::Err`.
|
||||
/// * If the request does not need any particular response, it will be
|
||||
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
|
||||
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
|
||||
pub type Reply = Result<Response, String>;
|
||||
|
||||
/// Successful response from niri to client.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Response {
|
||||
/// A request that does not need a response was handled successfully.
|
||||
Handled,
|
||||
/// Information about connected outputs.
|
||||
///
|
||||
/// Map from connector name to output info.
|
||||
Outputs(HashMap<String, Output>),
|
||||
}
|
||||
|
||||
/// Actions that niri can perform.
|
||||
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
|
||||
// variants from niri-config should be present here.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::Parser))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
|
||||
pub enum Action {
|
||||
/// Exit niri.
|
||||
Quit {
|
||||
/// Skip the "Press Enter to confirm" prompt.
|
||||
#[cfg_attr(feature = "clap", arg(short, long))]
|
||||
skip_confirmation: bool,
|
||||
},
|
||||
/// Power off all monitors via DPMS.
|
||||
PowerOffMonitors,
|
||||
/// Spawn a command.
|
||||
Spawn {
|
||||
/// Command to spawn.
|
||||
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
||||
command: Vec<String>,
|
||||
},
|
||||
/// Open the screenshot UI.
|
||||
Screenshot,
|
||||
/// Screenshot the focused screen.
|
||||
ScreenshotScreen,
|
||||
/// Screenshot the focused window.
|
||||
ScreenshotWindow,
|
||||
/// Close the focused window.
|
||||
CloseWindow,
|
||||
/// Toggle fullscreen on the focused window.
|
||||
FullscreenWindow,
|
||||
/// Focus the column to the left.
|
||||
FocusColumnLeft,
|
||||
/// Focus the column to the right.
|
||||
FocusColumnRight,
|
||||
/// Focus the first column.
|
||||
FocusColumnFirst,
|
||||
/// Focus the last column.
|
||||
FocusColumnLast,
|
||||
/// Focus the window below.
|
||||
FocusWindowDown,
|
||||
/// Focus the window above.
|
||||
FocusWindowUp,
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceDown,
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceUp,
|
||||
/// Move the focused column to the left.
|
||||
MoveColumnLeft,
|
||||
/// Move the focused column to the right.
|
||||
MoveColumnRight,
|
||||
/// Move the focused column to the start of the workspace.
|
||||
MoveColumnToFirst,
|
||||
/// Move the focused column to the end of the workspace.
|
||||
MoveColumnToLast,
|
||||
/// Move the focused window down in a column.
|
||||
MoveWindowDown,
|
||||
/// Move the focused window up in a column.
|
||||
MoveWindowUp,
|
||||
/// Move the focused window down in a column or to the workspace below.
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
/// Move the focused window up in a column or to the workspace above.
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
/// Consume or expel the focused window left.
|
||||
ConsumeOrExpelWindowLeft,
|
||||
/// Consume or expel the focused window right.
|
||||
ConsumeOrExpelWindowRight,
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn,
|
||||
/// Expel the focused window from the column.
|
||||
ExpelWindowFromColumn,
|
||||
/// Center the focused column on the screen.
|
||||
CenterColumn,
|
||||
/// Focus the workspace below.
|
||||
FocusWorkspaceDown,
|
||||
/// Focus the workspace above.
|
||||
FocusWorkspaceUp,
|
||||
/// Focus a workspace by index.
|
||||
FocusWorkspace {
|
||||
/// Index of the workspace to focus.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused window to the workspace below.
|
||||
MoveWindowToWorkspaceDown,
|
||||
/// Move the focused window to the workspace above.
|
||||
MoveWindowToWorkspaceUp,
|
||||
/// Move the focused window to a workspace by index.
|
||||
MoveWindowToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused column to the workspace below.
|
||||
MoveColumnToWorkspaceDown,
|
||||
/// Move the focused column to the workspace above.
|
||||
MoveColumnToWorkspaceUp,
|
||||
/// Move the focused column to a workspace by index.
|
||||
MoveColumnToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused workspace down.
|
||||
MoveWorkspaceDown,
|
||||
/// Move the focused workspace up.
|
||||
MoveWorkspaceUp,
|
||||
/// Focus the monitor to the left.
|
||||
FocusMonitorLeft,
|
||||
/// Focus the monitor to the right.
|
||||
FocusMonitorRight,
|
||||
/// Focus the monitor below.
|
||||
FocusMonitorDown,
|
||||
/// Focus the monitor above.
|
||||
FocusMonitorUp,
|
||||
/// Move the focused window to the monitor to the left.
|
||||
MoveWindowToMonitorLeft,
|
||||
/// Move the focused window to the monitor to the right.
|
||||
MoveWindowToMonitorRight,
|
||||
/// Move the focused window to the monitor below.
|
||||
MoveWindowToMonitorDown,
|
||||
/// Move the focused window to the monitor above.
|
||||
MoveWindowToMonitorUp,
|
||||
/// Move the focused column to the monitor to the left.
|
||||
MoveColumnToMonitorLeft,
|
||||
/// Move the focused column to the monitor to the right.
|
||||
MoveColumnToMonitorRight,
|
||||
/// Move the focused column to the monitor below.
|
||||
MoveColumnToMonitorDown,
|
||||
/// Move the focused column to the monitor above.
|
||||
MoveColumnToMonitorUp,
|
||||
/// Change the height of the focused window.
|
||||
SetWindowHeight {
|
||||
/// How to change the height.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Switch between preset column widths.
|
||||
SwitchPresetColumnWidth,
|
||||
/// Toggle the maximized state of the focused column.
|
||||
MaximizeColumn,
|
||||
/// Change the width of the focused column.
|
||||
SetColumnWidth {
|
||||
/// How to change the width.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Switch between keyboard layouts.
|
||||
SwitchLayout {
|
||||
/// Layout to switch to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
layout: LayoutSwitchTarget,
|
||||
},
|
||||
/// Show the hotkey overlay.
|
||||
ShowHotkeyOverlay,
|
||||
/// Move the focused workspace to the monitor to the left.
|
||||
MoveWorkspaceToMonitorLeft,
|
||||
/// Move the focused workspace to the monitor to the right.
|
||||
MoveWorkspaceToMonitorRight,
|
||||
/// Move the focused workspace to the monitor below.
|
||||
MoveWorkspaceToMonitorDown,
|
||||
/// Move the focused workspace to the monitor above.
|
||||
MoveWorkspaceToMonitorUp,
|
||||
/// Toggle a debug tint on windows.
|
||||
ToggleDebugTint,
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SizeChange {
|
||||
/// Set the size in logical pixels.
|
||||
SetFixed(i32),
|
||||
/// Set the size as a proportion of the working area.
|
||||
SetProportion(f64),
|
||||
/// Add or subtract to the current size in logical pixels.
|
||||
AdjustFixed(i32),
|
||||
/// Add or subtract to the current size as a proportion of the working area.
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
/// Layout to switch to.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LayoutSwitchTarget {
|
||||
/// The next configured layout.
|
||||
Next,
|
||||
/// The previous configured layout.
|
||||
Prev,
|
||||
}
|
||||
|
||||
/// Connected output.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Output {
|
||||
@@ -53,3 +258,55 @@ pub struct Mode {
|
||||
/// Refresh rate in millihertz.
|
||||
pub refresh_rate: u32,
|
||||
}
|
||||
|
||||
impl FromStr for SizeChange {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once('%') {
|
||||
Some((value, empty)) => {
|
||||
if !empty.is_empty() {
|
||||
return Err("trailing characters after '%' are not allowed");
|
||||
}
|
||||
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::AdjustProportion(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::SetProportion(value))
|
||||
}
|
||||
None => Err("value is missing"),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let value = s;
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::AdjustFixed(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::SetFixed(value))
|
||||
}
|
||||
None => Err("value is missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LayoutSwitchTarget {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"next" => Ok(Self::Next),
|
||||
"prev" => Ok(Self::Prev),
|
||||
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "niri-visual-tests"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.3", path = ".." }
|
||||
niri-config = { version = "0.1.3", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,14 @@
|
||||
# niri-visual-tests
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a development-only app, you shouldn't package it.
|
||||
|
||||
This app contains a number of hard-coded test scenarios for visual inspection.
|
||||
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
|
||||
The idea is to go through the test scenarios and check that everything *looks* right.
|
||||
|
||||
## Running
|
||||
|
||||
You will need recent GTK and libadwaita.
|
||||
Then, `cargo run`.
|
||||
@@ -0,0 +1,3 @@
|
||||
.anim-control-bar {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::f32::consts::{FRAC_PI_2, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::render_helpers::gradient::GradientRenderElement;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientAngle {
|
||||
angle: f32,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
impl GradientAngle {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
angle: 0.,
|
||||
prev_time: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientAngle {
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.angle += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.angle >= PI * 2. {
|
||||
self.angle -= PI * 2.
|
||||
}
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size);
|
||||
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
Scale::from(1.),
|
||||
area,
|
||||
area,
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
self.angle - FRAC_PI_2,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use std::f32::consts::{FRAC_PI_4, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::layout::focus_ring::FocusRing;
|
||||
use niri::render_helpers::gradient::GradientRenderElement;
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientArea {
|
||||
progress: f32,
|
||||
border: FocusRing,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
impl GradientArea {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
let mut border = FocusRing::new(niri_config::FocusRing {
|
||||
off: false,
|
||||
width: 1,
|
||||
active_color: Color::new(255, 255, 255, 128),
|
||||
inactive_color: Color::default(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
});
|
||||
border.set_active(true);
|
||||
|
||||
Self {
|
||||
progress: 0.,
|
||||
border,
|
||||
prev_time: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientArea {
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.progress += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.progress >= PI * 2. {
|
||||
self.progress -= PI * 2.
|
||||
}
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let mut rv = Vec::new();
|
||||
|
||||
let f = (self.progress.sin() + 1.) / 2.;
|
||||
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let rect_size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), rect_size);
|
||||
|
||||
let g_size = Size::from((
|
||||
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
|
||||
));
|
||||
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
|
||||
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
|
||||
|
||||
self.border.update(g_size, true);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(
|
||||
renderer,
|
||||
Point::from(g_loc),
|
||||
Scale::from(1.),
|
||||
size.to_logical(1),
|
||||
)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
rv.extend(
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
Scale::from(1.),
|
||||
area,
|
||||
g_area,
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
FRAC_PI_4,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::workspace::ColumnWidth;
|
||||
use niri::layout::Options;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
|
||||
pub struct Layout {
|
||||
output: Output,
|
||||
windows: Vec<TestWindow>,
|
||||
layout: niri::layout::Layout<TestWindow>,
|
||||
start_time: Duration,
|
||||
steps: HashMap<Duration, DynStepFn>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(size: Size<i32, Logical>) -> Self {
|
||||
let output = Output::new(
|
||||
String::new(),
|
||||
PhysicalProperties {
|
||||
size: Size::from((size.w, size.h)),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
},
|
||||
);
|
||||
let mode = Some(Mode {
|
||||
size: size.to_physical(1),
|
||||
refresh: 60000,
|
||||
});
|
||||
output.change_current_state(mode, None, None, None);
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 4,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
inactive_color: Color::new(50, 50, 50, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = niri::layout::Layout::with_options(options);
|
||||
layout.add_output(output.clone());
|
||||
|
||||
Self {
|
||||
output,
|
||||
windows: Vec::new(),
|
||||
layout,
|
||||
start_time: get_monotonic_time(),
|
||||
steps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.layout.activate_window(&rv.windows[0]);
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(&window);
|
||||
}
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
window: TestWindow,
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
self.layout
|
||||
.add_window_right_of(right_of, window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(&window);
|
||||
}
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
|
||||
self.steps
|
||||
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Layout {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let mode = Some(Mode {
|
||||
size: Size::from((width, height)),
|
||||
refresh: 60000,
|
||||
});
|
||||
self.output.change_current_state(mode, None, None, None);
|
||||
layer_map_for_output(&self.output).arrange();
|
||||
self.layout.update_output_size(&self.output);
|
||||
for win in &self.windows {
|
||||
if win.communicate() {
|
||||
self.layout.update_window(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.are_animations_ongoing()
|
||||
|| !self.steps.is_empty()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, mut current_time: Duration) {
|
||||
let run = self
|
||||
.steps
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|delay| self.start_time + *delay <= current_time)
|
||||
.collect::<Vec<_>>();
|
||||
for key in &run {
|
||||
let f = self.steps.remove(key).unwrap();
|
||||
f(self);
|
||||
}
|
||||
if !run.is_empty() {
|
||||
current_time = get_monotonic_time();
|
||||
}
|
||||
|
||||
self.layout.advance_animations(current_time);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
pub mod gradient_angle;
|
||||
pub mod gradient_area;
|
||||
pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
pub trait TestCase {
|
||||
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn advance_animations(&mut self, _current_time: Duration) {}
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Tile {
|
||||
window: TestWindow,
|
||||
tile: niri::layout::tile::Tile<TestWindow>,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::freeform(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn with_window(window: TestWindow) -> Self {
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 32,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
|
||||
Self { window, tile }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.tile.request_tile_size(Size::from((width, height)));
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.tile.are_animations_ongoing()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.tile.advance_animations(current_time, true);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let tile_size = self.tile.tile_size().to_physical(1);
|
||||
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
|
||||
|
||||
self.tile
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
size.to_logical(1),
|
||||
true,
|
||||
)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use niri::layout::LayoutElement;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Window {
|
||||
window: TestWindow,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window.request_size(Size::from((width, height)));
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let win_size = self.window.size().to_physical(1);
|
||||
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
|
||||
|
||||
self.window
|
||||
.render(renderer, location, Scale::from(1.))
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::env;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||
use cases::tile::Tile;
|
||||
use cases::window::Window;
|
||||
use gtk::prelude::{
|
||||
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
||||
};
|
||||
use gtk::{gdk, gio, glib};
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay_view::SmithayView;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cases::gradient_angle::GradientAngle;
|
||||
use crate::cases::gradient_area::GradientArea;
|
||||
use crate::cases::layout::Layout;
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod cases;
|
||||
mod smithay_view;
|
||||
mod test_window;
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
let directives =
|
||||
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
|
||||
app.connect_startup(on_startup);
|
||||
app.connect_activate(build_ui);
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn on_startup(_app: &adw::Application) {
|
||||
// Load our CSS.
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_string(include_str!("../resources/style.css"));
|
||||
if let Some(display) = gdk::Display::default() {
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ui(app: &adw::Application) {
|
||||
let stack = gtk::Stack::new();
|
||||
|
||||
struct S {
|
||||
stack: gtk::Stack,
|
||||
}
|
||||
|
||||
impl S {
|
||||
fn add<T: TestCase + 'static>(
|
||||
&self,
|
||||
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
title: &str,
|
||||
) {
|
||||
let view = SmithayView::new(make);
|
||||
self.stack.add_titled(&view, None, title);
|
||||
}
|
||||
}
|
||||
|
||||
let s = S {
|
||||
stack: stack.clone(),
|
||||
};
|
||||
|
||||
s.add(Window::freeform, "Freeform Window");
|
||||
s.add(Window::fixed_size, "Fixed Size Window");
|
||||
s.add(
|
||||
Window::fixed_size_with_csd_shadow,
|
||||
"Fixed Size Window - CSD Shadow",
|
||||
);
|
||||
|
||||
s.add(Tile::freeform, "Freeform Tile");
|
||||
s.add(Tile::fixed_size, "Fixed Size Tile");
|
||||
s.add(
|
||||
Tile::fixed_size_with_csd_shadow,
|
||||
"Fixed Size Tile - CSD Shadow",
|
||||
);
|
||||
s.add(Tile::freeform_open, "Freeform Tile - Open");
|
||||
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
|
||||
s.add(
|
||||
Tile::fixed_size_with_csd_shadow_open,
|
||||
"Fixed Size Tile - CSD Shadow - Open",
|
||||
);
|
||||
|
||||
s.add(Layout::open_in_between, "Layout - Open In-Between");
|
||||
s.add(
|
||||
Layout::open_multiple_quickly,
|
||||
"Layout - Open Multiple Quickly",
|
||||
);
|
||||
s.add(
|
||||
Layout::open_multiple_quickly_big,
|
||||
"Layout - Open Multiple Quickly - Big",
|
||||
);
|
||||
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
|
||||
s.add(
|
||||
Layout::open_to_the_left_big,
|
||||
"Layout - Open To The Left - Big",
|
||||
);
|
||||
|
||||
s.add(GradientAngle::new, "Gradient - Angle");
|
||||
s.add(GradientArea::new, "Gradient - Area");
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
anim_adjustment
|
||||
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
|
||||
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||
anim_scale.set_hexpand(true);
|
||||
|
||||
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
anim_control_bar.add_css_class("anim-control-bar");
|
||||
anim_control_bar.append(>k::Label::new(Some("Slowdown")));
|
||||
anim_control_bar.append(&anim_scale);
|
||||
|
||||
let content_view = adw::ToolbarView::new();
|
||||
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||
content_view.add_top_bar(&content_headerbar);
|
||||
content_view.add_bottom_bar(&anim_control_bar);
|
||||
content_view.set_content(Some(&stack));
|
||||
let content = adw::NavigationPage::new(
|
||||
&content_view,
|
||||
stack
|
||||
.page(&stack.visible_child().unwrap())
|
||||
.title()
|
||||
.as_deref()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let sidebar_header = adw::HeaderBar::new();
|
||||
let stack_sidebar = gtk::StackSidebar::new();
|
||||
stack_sidebar.set_stack(&stack);
|
||||
let sidebar_view = adw::ToolbarView::new();
|
||||
sidebar_view.add_top_bar(&sidebar_header);
|
||||
sidebar_view.set_content(Some(&stack_sidebar));
|
||||
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
|
||||
|
||||
let split_view = adw::NavigationSplitView::new();
|
||||
split_view.set_content(Some(&content));
|
||||
split_view.set_sidebar(Some(&sidebar));
|
||||
|
||||
stack.connect_visible_child_notify(move |stack| {
|
||||
content.set_title(
|
||||
stack
|
||||
.visible_child()
|
||||
.and_then(|c| stack.page(&c).title())
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = adw::ApplicationWindow::new(app);
|
||||
window.set_title(Some("niri visual tests"));
|
||||
window.set_content(Some(&split_view));
|
||||
window.present();
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use gtk::glib;
|
||||
use gtk::subclass::prelude::*;
|
||||
use smithay::utils::{Logical, Size};
|
||||
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::ptr::null;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use niri::render_helpers::shaders;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||
use smithay::backend::renderer::{Frame, Renderer, Unbind};
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
gl_area: gtk::GLArea,
|
||||
size: Cell<(i32, i32)>,
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for SmithayView {
|
||||
const NAME: &'static str = "NiriSmithayView";
|
||||
type Type = super::SmithayView;
|
||||
type ParentType = gtk::Widget;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for SmithayView {
|
||||
fn constructed(&self) {
|
||||
let obj = self.obj();
|
||||
|
||||
self.parent_constructed();
|
||||
|
||||
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
|
||||
self.gl_area.set_parent(&*obj);
|
||||
|
||||
self.gl_area.connect_resize({
|
||||
let imp = self.downgrade();
|
||||
move |_, width, height| {
|
||||
if let Some(imp) = imp.upgrade() {
|
||||
imp.resize(width, height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.gl_area.connect_render({
|
||||
let imp = self.downgrade();
|
||||
move |_, gl_context| {
|
||||
if let Some(imp) = imp.upgrade() {
|
||||
if let Err(err) = imp.render(gl_context) {
|
||||
warn!("error rendering: {err:?}");
|
||||
}
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
});
|
||||
|
||||
obj.add_tick_callback(|obj, _frame_clock| {
|
||||
let imp = obj.imp();
|
||||
|
||||
if let Some(case) = &mut *imp.test_case.borrow_mut() {
|
||||
if case.are_animations_ongoing() {
|
||||
imp.gl_area.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.gl_area.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for SmithayView {
|
||||
fn unmap(&self) {
|
||||
self.test_case.replace(None);
|
||||
self.parent_unmap();
|
||||
}
|
||||
|
||||
fn unrealize(&self) {
|
||||
self.renderer.replace(None);
|
||||
self.parent_unrealize();
|
||||
}
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
fn resize(&self, width: i32, height: i32) {
|
||||
self.size.set((width, height));
|
||||
|
||||
if let Some(case) = &mut *self.test_case.borrow_mut() {
|
||||
case.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
|
||||
// Set up the Smithay renderer.
|
||||
let mut renderer = self.renderer.borrow_mut();
|
||||
let renderer = renderer.get_or_insert_with(|| {
|
||||
unsafe { create_renderer() }
|
||||
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
|
||||
});
|
||||
let Ok(renderer) = renderer else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
// Create the test case if missing.
|
||||
let mut case = self.test_case.borrow_mut();
|
||||
let case = case.get_or_insert_with(|| {
|
||||
let make = self.make_test_case.get().unwrap();
|
||||
make(Size::from(size))
|
||||
});
|
||||
|
||||
case.advance_animations(get_monotonic_time());
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
|
||||
|
||||
let elements = unsafe {
|
||||
with_framebuffer_save_restore(renderer, |renderer| {
|
||||
case.render(renderer, Size::from(size))
|
||||
})
|
||||
}?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(rect.size, Transform::Normal)
|
||||
.context("error creating frame")?;
|
||||
|
||||
frame
|
||||
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements.iter().rev() {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(Scale::from(1.));
|
||||
|
||||
if let Some(mut damage) = rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
||||
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||
.context("error loading EGL symbols in Smithay")?;
|
||||
|
||||
let egl_display = egl::GetCurrentDisplay();
|
||||
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
|
||||
|
||||
let egl_context = egl::GetCurrentContext();
|
||||
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
|
||||
|
||||
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
|
||||
// really use it anyway so just get some random one.
|
||||
let mut egl_config_id = null();
|
||||
let mut num_configs = 0;
|
||||
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
|
||||
ensure!(res == egl::TRUE, "error choosing EGL config");
|
||||
ensure!(num_configs != 0, "no EGL config");
|
||||
|
||||
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
|
||||
.context("error creating EGL context")?;
|
||||
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
|
||||
.context("error getting supported renderer capabilities")?
|
||||
.into_iter()
|
||||
.filter(|c| *c != Capability::ColorTransformations);
|
||||
|
||||
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
|
||||
.context("error creating GlesRenderer")?;
|
||||
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
Ok(renderer)
|
||||
}
|
||||
|
||||
unsafe fn with_framebuffer_save_restore<T>(
|
||||
renderer: &mut GlesRenderer,
|
||||
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
let rv = f(renderer);
|
||||
|
||||
renderer.unbind().context("error unbinding")?;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||
@extends gtk::Widget;
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
pub fn new<T: TestCase + 'static>(
|
||||
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder().build();
|
||||
|
||||
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
|
||||
let make_test_case = Box::new(make) as _;
|
||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||
|
||||
obj
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{LayoutElement, LayoutElementRenderElement};
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Size, Transform};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestWindowInner {
|
||||
id: usize,
|
||||
size: Size<i32, Logical>,
|
||||
requested_size: Option<Size<i32, Logical>>,
|
||||
min_size: Size<i32, Logical>,
|
||||
max_size: Size<i32, Logical>,
|
||||
buffer: SolidColorBuffer,
|
||||
pending_fullscreen: bool,
|
||||
csd_shadow_width: i32,
|
||||
csd_shadow_buffer: SolidColorBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
|
||||
|
||||
impl TestWindow {
|
||||
pub fn freeform(id: usize) -> Self {
|
||||
let size = Size::from((100, 200));
|
||||
let min_size = Size::from((0, 0));
|
||||
let max_size = Size::from((0, 0));
|
||||
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
|
||||
|
||||
Self(Rc::new(RefCell::new(TestWindowInner {
|
||||
id,
|
||||
size,
|
||||
requested_size: None,
|
||||
min_size,
|
||||
max_size,
|
||||
buffer,
|
||||
pending_fullscreen: false,
|
||||
csd_shadow_width: 0,
|
||||
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn fixed_size(id: usize) -> Self {
|
||||
let rv = Self::freeform(id);
|
||||
rv.set_min_size((200, 400).into());
|
||||
rv.set_max_size((200, 400).into());
|
||||
rv.set_color([0.88, 0.11, 0.14, 1.]);
|
||||
rv.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn set_min_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().min_size = size;
|
||||
}
|
||||
|
||||
pub fn set_max_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().max_size = size;
|
||||
}
|
||||
|
||||
pub fn set_color(&self, color: [f32; 4]) {
|
||||
self.0.borrow_mut().buffer.set_color(color);
|
||||
}
|
||||
|
||||
pub fn set_csd_shadow_width(&self, width: i32) {
|
||||
self.0.borrow_mut().csd_shadow_width = width;
|
||||
}
|
||||
|
||||
pub fn communicate(&self) -> bool {
|
||||
let mut rv = false;
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
let mut new_size = inner.size;
|
||||
|
||||
if let Some(size) = inner.requested_size.take() {
|
||||
assert!(size.w >= 0);
|
||||
assert!(size.h >= 0);
|
||||
|
||||
if size.w != 0 {
|
||||
new_size.w = size.w;
|
||||
}
|
||||
if size.h != 0 {
|
||||
new_size.h = size.h;
|
||||
}
|
||||
}
|
||||
|
||||
if inner.max_size.w > 0 {
|
||||
new_size.w = min(new_size.w, inner.max_size.w);
|
||||
}
|
||||
if inner.max_size.h > 0 {
|
||||
new_size.h = min(new_size.h, inner.max_size.h);
|
||||
}
|
||||
if inner.min_size.w > 0 {
|
||||
new_size.w = max(new_size.w, inner.min_size.w);
|
||||
}
|
||||
if inner.min_size.h > 0 {
|
||||
new_size.h = max(new_size.h, inner.min_size.h);
|
||||
}
|
||||
|
||||
if inner.size != new_size {
|
||||
inner.size = new_size;
|
||||
inner.buffer.resize(new_size);
|
||||
rv = true;
|
||||
}
|
||||
|
||||
let mut csd_shadow_size = new_size;
|
||||
csd_shadow_size.w += inner.csd_shadow_width * 2;
|
||||
csd_shadow_size.h += inner.csd_shadow_width * 2;
|
||||
inner.csd_shadow_buffer.resize(csd_shadow_size);
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TestWindow {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.borrow().id == other.0.borrow().id
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutElement for TestWindow {
|
||||
fn size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().size
|
||||
}
|
||||
|
||||
fn buf_loc(&self) -> Point<i32, Logical> {
|
||||
(0, 0).into()
|
||||
}
|
||||
|
||||
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
let inner = self.0.borrow();
|
||||
|
||||
vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
|
||||
.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().requested_size = Some(size);
|
||||
self.0.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().pending_fullscreen = true;
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().min_size
|
||||
}
|
||||
|
||||
fn max_size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().max_size
|
||||
}
|
||||
|
||||
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
|
||||
|
||||
fn has_ssd(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn output_enter(&self, _output: &Output) {}
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
self.0.borrow().pending_fullscreen
|
||||
}
|
||||
}
|
||||
+240
-15
@@ -28,6 +28,7 @@ input {
|
||||
touchpad {
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
@@ -40,6 +41,12 @@ input {
|
||||
// accel-profile "flat"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
}
|
||||
|
||||
tablet {
|
||||
// Set the name of the output (see below) which the tablet will map to.
|
||||
// If this is unset or the output doesn't exist, the tablet maps to one of the
|
||||
@@ -47,6 +54,13 @@ input {
|
||||
map-to-output "eDP-1"
|
||||
}
|
||||
|
||||
touch {
|
||||
// Set the name of the output (see below) which touch input will map to.
|
||||
// If this is unset or the output doesn't exist, touch input maps to one of the
|
||||
// existing outputs.
|
||||
map-to-output "eDP-1"
|
||||
}
|
||||
|
||||
// By default, niri will take over the power button to make it sleep
|
||||
// instead of power off.
|
||||
// Uncomment this if you would like to configure the power button elsewhere
|
||||
@@ -57,7 +71,7 @@ input {
|
||||
// You can configure outputs by their name, which you can find
|
||||
// by running `niri msg outputs` while inside a niri instance.
|
||||
// The built-in laptop monitor is usually called "eDP-1".
|
||||
// Remember to uncommend the node by removing "/-"!
|
||||
// Remember to uncomment the node by removing "/-"!
|
||||
/-output "eDP-1" {
|
||||
// Uncomment this line to disable this output.
|
||||
// off
|
||||
@@ -65,13 +79,17 @@ input {
|
||||
// Scale is a floating-point number, but at the moment only integer values work.
|
||||
scale 2.0
|
||||
|
||||
// Transform allows to rotate the output counter-clockwise, valid values are:
|
||||
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
|
||||
transform "normal"
|
||||
|
||||
// Resolution and, optionally, refresh rate of the output.
|
||||
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
|
||||
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
||||
// for the resolution.
|
||||
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
||||
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||
mode "1920x1080@144"
|
||||
mode "1920x1080@120.030"
|
||||
|
||||
// Position of the output in the global coordinate space.
|
||||
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
|
||||
@@ -86,6 +104,14 @@ input {
|
||||
}
|
||||
|
||||
layout {
|
||||
// By default focus ring and border are rendered as a solid background rectangle
|
||||
// behind windows. That is, they will show up through semitransparent windows.
|
||||
// This is because windows using client-side decorations can have an arbitrary shape.
|
||||
//
|
||||
// If you don't like that, you should uncomment `prefer-no-csd` below.
|
||||
// Niri will draw focus ring and border *around* windows that agree to omit their
|
||||
// client-side decorations.
|
||||
|
||||
// You can change how the focus ring looks.
|
||||
focus-ring {
|
||||
// Uncomment this line to disable the focus ring.
|
||||
@@ -94,11 +120,33 @@ layout {
|
||||
// How many logical pixels the ring extends out from the windows.
|
||||
width 4
|
||||
|
||||
// Color of the ring on the active monitor: red, green, blue, alpha.
|
||||
active-color 127 200 255 255
|
||||
// Colors can be set in a variety of ways:
|
||||
// - CSS named colors: "red"
|
||||
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
|
||||
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
|
||||
|
||||
// Color of the ring on inactive monitors: red, green, blue, alpha.
|
||||
inactive-color 80 80 80 255
|
||||
// Color of the ring on the active monitor.
|
||||
active-color "#7fc8ff"
|
||||
|
||||
// Color of the ring on inactive monitors.
|
||||
inactive-color "#505050"
|
||||
|
||||
// Additionally, there's a legacy RGBA syntax:
|
||||
// active-color 127 200 255 255
|
||||
|
||||
// You can also use gradients. They take precedence over solid colors.
|
||||
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
|
||||
// The angle is the same as in linear-gradient, and is optional,
|
||||
// defaulting to 180 (top-to-bottom gradient).
|
||||
// You can use any CSS linear-gradient tool on the web to set these up.
|
||||
//
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
|
||||
// You can also color the gradient relative to the entire view
|
||||
// of the workspace, rather than relative to just the window itself.
|
||||
// To do that, set relative-to="workspace-view".
|
||||
//
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
// You can also add a border. It's similar to the focus ring, but always visible.
|
||||
@@ -108,8 +156,11 @@ layout {
|
||||
off
|
||||
|
||||
width 4
|
||||
active-color 255 200 127 255
|
||||
inactive-color 80 80 80 255
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||
@@ -117,9 +168,9 @@ layout {
|
||||
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
||||
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||
proportion 0.333
|
||||
proportion 0.33333
|
||||
proportion 0.5
|
||||
proportion 0.667
|
||||
proportion 0.66667
|
||||
|
||||
// Fixed sets the width in logical pixels exactly.
|
||||
// fixed 1920
|
||||
@@ -159,6 +210,15 @@ layout {
|
||||
// which may be more convenient to use.
|
||||
// spawn-at-startup "alacritty" "-e" "fish"
|
||||
|
||||
// You can override environment variables for processes spawned by niri.
|
||||
environment {
|
||||
// Set a variable like this:
|
||||
// QT_QPA_PLATFORM "wayland"
|
||||
|
||||
// Remove a variable by using null as the value:
|
||||
// DISPLAY null
|
||||
}
|
||||
|
||||
cursor {
|
||||
// Change the theme and size of the cursor as well as set the
|
||||
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
||||
@@ -185,6 +245,139 @@ hotkey-overlay {
|
||||
// skip-at-startup
|
||||
}
|
||||
|
||||
// Animation settings.
|
||||
animations {
|
||||
// Uncomment to turn off all animations.
|
||||
// off
|
||||
|
||||
// Slow down all animations by this factor. Values below 1 speed them up instead.
|
||||
// slowdown 3.0
|
||||
|
||||
// You can configure all individual animations.
|
||||
// Available settings are the same for all of them.
|
||||
// - off disables the animation.
|
||||
//
|
||||
// Niri supports two animation types: easing and spring.
|
||||
// You can set properties for only ONE of them.
|
||||
//
|
||||
// Easing has the following settings:
|
||||
// - duration-ms sets the duration of the animation in milliseconds.
|
||||
// - curve sets the easing curve. Currently, available curves
|
||||
// are "ease-out-cubic" and "ease-out-expo".
|
||||
//
|
||||
// Spring animations work better with touchpad gestures, because they
|
||||
// take into account the velocity of your fingers as you release the swipe.
|
||||
// The parameters are less obvious and generally should be tuned
|
||||
// with trial and error. Notably, you cannot directly set the duration.
|
||||
// You can use this app to help visualize how the spring parameters
|
||||
// change the animation: https://flathub.org/apps/app.drey.Elastic
|
||||
//
|
||||
// A spring animation is configured like this:
|
||||
// - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||
//
|
||||
// The damping ratio goes from 0.1 to 10.0 and has the following properties:
|
||||
// - below 1.0: underdamped spring, will oscillate in the end.
|
||||
// - above 1.0: overdamped spring, won't oscillate.
|
||||
// - 1.0: critically damped spring, comes to rest in minimum possible time
|
||||
// without oscillations.
|
||||
//
|
||||
// However, even with damping ratio = 1.0 the spring animation may oscillate
|
||||
// if "launched" with enough velocity from a touchpad swipe.
|
||||
//
|
||||
// Lower stiffness will result in a slower animation more prone to oscillation.
|
||||
//
|
||||
// Set epsilon to a lower value if the animation "jumps" in the end.
|
||||
//
|
||||
// The spring mass is hardcoded to 1.0 and cannot be changed. Instead, change
|
||||
// stiffness proportionally. E.g. increasing mass by 2x is the same as
|
||||
// decreasing stiffness by 2x.
|
||||
|
||||
// Animation when switching workspaces up and down,
|
||||
// including after the touchpad gesture.
|
||||
workspace-switch {
|
||||
// off
|
||||
// spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||
}
|
||||
|
||||
// All horizontal camera view movement:
|
||||
// - When a window off-screen is focused and the camera scrolls to it.
|
||||
// - When a new window appears off-screen and the camera scrolls to it.
|
||||
// - When a window resizes bigger and the camera scrolls to show it in full.
|
||||
// - And so on.
|
||||
horizontal-view-movement {
|
||||
// off
|
||||
// spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||
}
|
||||
|
||||
// Window opening animation. Note that this one has different defaults.
|
||||
window-open {
|
||||
// off
|
||||
// duration-ms 150
|
||||
// curve "ease-out-expo"
|
||||
|
||||
// Example for a slightly bouncy window opening:
|
||||
// spring damping-ratio=0.8 stiffness=1000 epsilon=0.0001
|
||||
}
|
||||
|
||||
// Config parse error and new default config creation notification
|
||||
// open/close animation.
|
||||
config-notification-open-close {
|
||||
// off
|
||||
// spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
|
||||
}
|
||||
}
|
||||
|
||||
// Window rules let you adjust behavior for individual windows.
|
||||
// They are processed in order of appearance in this file.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
// Match directives control which windows this rule will apply to.
|
||||
// You can match by app-id and by title.
|
||||
// The window must match all properties of the match directive.
|
||||
match app-id="org.myapp.MyApp" title="My Cool App"
|
||||
|
||||
// There can be multiple match directives. A window must match any one
|
||||
// of the rule's match directives.
|
||||
//
|
||||
// If there are no match directives, any window will match the rule.
|
||||
match title="Second App"
|
||||
|
||||
// You can also add exclude directives which have the same properties.
|
||||
// If a window matches any exclude directive, it won't match this rule.
|
||||
//
|
||||
// Both app-id and title are regular expressions.
|
||||
// Raw KDL strings are helpful here.
|
||||
exclude app-id=r#"\.unwanted\."#
|
||||
|
||||
// Here are the properties that you can set on a window rule.
|
||||
// You can override the default column width.
|
||||
default-column-width { proportion 0.75; }
|
||||
|
||||
// You can set the output that this window will initially open on.
|
||||
// If such an output does not exist, it will open on the currently
|
||||
// focused output as usual.
|
||||
open-on-output "eDP-1"
|
||||
|
||||
// Make this window open as a maximized column.
|
||||
open-maximized true
|
||||
|
||||
// Make this window open fullscreen.
|
||||
open-fullscreen true
|
||||
// You can also set this to false to prevent a window from opening fullscreen.
|
||||
// open-fullscreen false
|
||||
}
|
||||
|
||||
// Here's a useful example. Work around WezTerm's initial configure bug
|
||||
// by setting an empty default-column-width.
|
||||
window-rule {
|
||||
// This regular expression is intentionally made as specific as possible,
|
||||
// since this is the default config, and we want no false positives.
|
||||
// You can get away with just app-id="wezterm" if you want.
|
||||
// The regular expression can match anywhere in the string.
|
||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||
default-column-width {}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -192,6 +385,9 @@ binds {
|
||||
//
|
||||
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
||||
// when running as a winit window.
|
||||
//
|
||||
// Most actions that you can bind here can also be invoked programmatically with
|
||||
// `niri msg action do-something`.
|
||||
|
||||
// Mod-Shift-/, which is usually the same as Mod-?,
|
||||
// shows a list of important hotkeys.
|
||||
@@ -200,7 +396,7 @@ binds {
|
||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||
Mod+T { spawn "alacritty"; }
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Mod+Alt+L { spawn "swaylock"; }
|
||||
Super+Alt+L { spawn "swaylock"; }
|
||||
|
||||
// You can also use a shell:
|
||||
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||
@@ -263,6 +459,10 @@ binds {
|
||||
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||
// ...
|
||||
|
||||
// And you can also move a whole workspace to another monitor:
|
||||
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
|
||||
// ...
|
||||
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+U { focus-workspace-down; }
|
||||
@@ -281,6 +481,14 @@ binds {
|
||||
Mod+Shift+U { move-workspace-down; }
|
||||
Mod+Shift+I { move-workspace-up; }
|
||||
|
||||
// You can refer to workspaces by index. However, keep in mind that
|
||||
// niri is a dynamic workspace system, so these commands are kind of
|
||||
// "best effort". Trying to refer to a workspace index bigger than
|
||||
// the current workspace count will instead refer to the bottommost
|
||||
// (empty) workspace.
|
||||
//
|
||||
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
|
||||
// will all refer to the 3rd workspace.
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+2 { focus-workspace 2; }
|
||||
Mod+3 { focus-workspace 3; }
|
||||
@@ -306,6 +514,10 @@ binds {
|
||||
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.
|
||||
// Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
// Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
@@ -338,10 +550,17 @@ binds {
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
// If you want to skip the confirmation dialog, set the flag like so:
|
||||
// Mod+Shift+E { quit skip-confirmation=true; }
|
||||
Mod+Shift+E { quit; }
|
||||
|
||||
Mod+Shift+P { power-off-monitors; }
|
||||
|
||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||
// This debug bind will tint all surfaces green, unless they are being
|
||||
// directly scanned out. It's therefore useful to check if direct scanout
|
||||
// is working.
|
||||
// Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||
}
|
||||
|
||||
// Settings for debugging. Not meant for normal use.
|
||||
@@ -364,9 +583,15 @@ debug {
|
||||
// The cursor will be rendered together with the rest of the frame.
|
||||
// disable-cursor-plane
|
||||
|
||||
// Slow down animations by this factor.
|
||||
// animation-slowdown 3.0
|
||||
|
||||
// Override the DRM device that niri will use for all rendering.
|
||||
// render-drm-device "/dev/dri/renderD129"
|
||||
|
||||
// Enable the color-transformations capability of the Smithay renderer.
|
||||
// May cause a slight decrease in rendering performance.
|
||||
// enable-color-transformations-capability
|
||||
|
||||
// Emulate zero (unknown) presentation time returned from DRM.
|
||||
// This is a thing on NVIDIA proprietary drivers, so this flag can be
|
||||
// used to test that we don't break too hard on those systems.
|
||||
// emulate-zero-presentation-time
|
||||
}
|
||||
|
||||
@@ -20,12 +20,6 @@ fi
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Set the current desktop for xdg-desktop-portal.
|
||||
export XDG_CURRENT_DESKTOP=niri
|
||||
|
||||
# Ensure the session type is set to Wayland for xdg-autostart apps.
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
@@ -44,4 +38,4 @@ systemctl --user --wait start niri.service
|
||||
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
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
|
||||
@@ -9,5 +9,6 @@ Wants=xdg-desktop-autostart.target
|
||||
Before=xdg-desktop-autostart.target
|
||||
|
||||
[Service]
|
||||
Slice=session.slice
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/niri
|
||||
ExecStart=/usr/bin/niri --session
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use keyframe::functions::EaseOutCubic;
|
||||
use keyframe::EasingFunction;
|
||||
use portable_atomic::{AtomicF64, Ordering};
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Animation {
|
||||
from: f64,
|
||||
to: f64,
|
||||
duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(from: f64, to: f64, over: Duration) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_time(&mut self, time: Duration) {
|
||||
self.current_time = time;
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
let passed = (self.current_time - self.start_time).as_secs_f64();
|
||||
let total = self.duration.as_secs_f64();
|
||||
let x = (passed / total).clamp(0., 1.);
|
||||
EaseOutCubic.y(x) * (self.to - self.from) + self.from
|
||||
}
|
||||
|
||||
pub fn to(&self) -> f64 {
|
||||
self.to
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use keyframe::functions::EaseOutCubic;
|
||||
use keyframe::EasingFunction;
|
||||
use portable_atomic::{AtomicF64, Ordering};
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
mod spring;
|
||||
pub use spring::{Spring, SpringParams};
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Animation {
|
||||
from: f64,
|
||||
to: f64,
|
||||
duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Kind {
|
||||
Easing {
|
||||
curve: Curve,
|
||||
},
|
||||
Spring(Spring),
|
||||
Deceleration {
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Curve {
|
||||
EaseOutCubic,
|
||||
EaseOutExpo,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
default: niri_config::Animation,
|
||||
) -> Self {
|
||||
if config.off {
|
||||
return Self::ease(from, to, 0, Curve::EaseOutCubic);
|
||||
}
|
||||
|
||||
// Resolve defaults.
|
||||
let (kind, easing_defaults) = match (config.kind, default.kind) {
|
||||
// Configured spring.
|
||||
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
|
||||
// Configured nothing, defaults spring.
|
||||
(
|
||||
niri_config::AnimationKind::Easing(easing),
|
||||
defaults @ niri_config::AnimationKind::Spring(_),
|
||||
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
|
||||
// Configured easing or nothing, defaults easing.
|
||||
(
|
||||
configured @ niri_config::AnimationKind::Easing(_),
|
||||
niri_config::AnimationKind::Easing(defaults),
|
||||
) => (configured, Some(defaults)),
|
||||
// Configured easing, defaults spring.
|
||||
(
|
||||
configured @ niri_config::AnimationKind::Easing(_),
|
||||
niri_config::AnimationKind::Spring(_),
|
||||
) => (configured, None),
|
||||
};
|
||||
|
||||
match kind {
|
||||
niri_config::AnimationKind::Spring(p) => {
|
||||
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
||||
|
||||
let spring = Spring {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
params,
|
||||
};
|
||||
Self::spring(spring)
|
||||
}
|
||||
niri_config::AnimationKind::Easing(p) => {
|
||||
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
|
||||
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
|
||||
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
|
||||
Self::ease(from, to, u64::from(duration_ms), curve)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ease(from: f64, to: f64, duration_ms: u64, curve: Curve) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
let kind = Kind::Easing { curve };
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spring(spring: Spring) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration = spring.duration();
|
||||
let kind = Kind::Spring(spring);
|
||||
|
||||
Self {
|
||||
from: spring.from,
|
||||
to: spring.to,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decelerate(
|
||||
from: f64,
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
threshold: f64,
|
||||
) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration_s = if initial_velocity == 0. {
|
||||
0.
|
||||
} else {
|
||||
let coeff = 1000. * deceleration_rate.ln();
|
||||
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
|
||||
};
|
||||
let duration = Duration::from_secs_f64(duration_s);
|
||||
|
||||
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
|
||||
|
||||
let kind = Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
};
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_time(&mut self, time: Duration) {
|
||||
if self.duration.is_zero() {
|
||||
self.current_time = time;
|
||||
return;
|
||||
}
|
||||
|
||||
let end_time = self.start_time + self.duration;
|
||||
if end_time <= self.current_time {
|
||||
return;
|
||||
}
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
if slowdown <= f64::EPSILON {
|
||||
// Zero slowdown will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't change current_time (since the incoming time values are always real-time), so
|
||||
// apply the slowdown by shifting the start time to compensate.
|
||||
if self.current_time <= time {
|
||||
let delta = time - self.current_time;
|
||||
|
||||
let max_delta = end_time - self.current_time;
|
||||
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Our slowdown value will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time -= adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time += delta - adjusted_delta;
|
||||
}
|
||||
} else {
|
||||
let delta = self.current_time - time;
|
||||
|
||||
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Current time was about to jump to before the animation had started; let's just
|
||||
// cancel the animation in this case.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time += adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time -= delta - adjusted_delta;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_time = time;
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
if self.is_done() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.current_time - self.start_time;
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
let passed = passed.as_secs_f64();
|
||||
let total = self.duration.as_secs_f64();
|
||||
let x = (passed / total).clamp(0., 1.);
|
||||
curve.y(x) * (self.to - self.from) + self.from
|
||||
}
|
||||
Kind::Spring(spring) => spring.value_at(passed),
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let passed = passed.as_secs_f64();
|
||||
let coeff = 1000. * deceleration_rate.ln();
|
||||
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to(&self) -> f64 {
|
||||
self.to
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub fn y(self, x: f64) -> f64 {
|
||||
match self {
|
||||
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<niri_config::AnimationCurve> for Curve {
|
||||
fn from(value: niri_config::AnimationCurve) -> Self {
|
||||
match value {
|
||||
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
||||
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SpringParams {
|
||||
pub damping: f64,
|
||||
pub mass: f64,
|
||||
pub stiffness: f64,
|
||||
pub epsilon: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Spring {
|
||||
pub from: f64,
|
||||
pub to: f64,
|
||||
pub initial_velocity: f64,
|
||||
pub params: SpringParams,
|
||||
}
|
||||
|
||||
impl SpringParams {
|
||||
pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self {
|
||||
let damping_ratio = damping_ratio.max(0.);
|
||||
let stiffness = stiffness.max(0.);
|
||||
let epsilon = epsilon.max(0.);
|
||||
|
||||
let mass = 1.;
|
||||
let critical_damping = 2. * (mass * stiffness).sqrt();
|
||||
let damping = damping_ratio * critical_damping;
|
||||
|
||||
Self {
|
||||
damping,
|
||||
mass,
|
||||
stiffness,
|
||||
epsilon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Spring {
|
||||
pub fn value_at(&self, t: Duration) -> f64 {
|
||||
self.oscillate(t.as_secs_f64())
|
||||
}
|
||||
|
||||
// Based on libadwaita (LGPL-2.1-or-later):
|
||||
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c,
|
||||
// which itself is based on (MIT):
|
||||
// https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
|
||||
/// Computes and returns the duration until the spring is at rest.
|
||||
pub fn duration(&self) -> Duration {
|
||||
const DELTA: f64 = 0.001;
|
||||
|
||||
let beta = self.params.damping / (2. * self.params.mass);
|
||||
|
||||
if beta.abs() <= f64::EPSILON || beta < 0. {
|
||||
return Duration::MAX;
|
||||
}
|
||||
|
||||
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||
|
||||
// As first ansatz for the overdamped solution,
|
||||
// and general estimation for the oscillating ones
|
||||
// we take the value of the envelope when it's < epsilon.
|
||||
let mut x0 = -self.params.epsilon.ln() / beta;
|
||||
|
||||
// f64::EPSILON is too small for this specific comparison, so we use
|
||||
// f32::EPSILON even though it's doubles.
|
||||
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
|
||||
return Duration::from_secs_f64(x0);
|
||||
}
|
||||
|
||||
// Since the overdamped solution decays way slower than the envelope
|
||||
// we need to use the value of the oscillation itself.
|
||||
// Newton's root finding method is a good candidate in this particular case:
|
||||
// https://en.wikipedia.org/wiki/Newton%27s_method
|
||||
let mut y0 = self.oscillate(x0);
|
||||
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||
|
||||
let mut x1 = (self.to - y0 + m * x0) / m;
|
||||
let mut y1 = self.oscillate(x1);
|
||||
|
||||
let mut i = 0;
|
||||
while (self.to - y1).abs() > self.params.epsilon {
|
||||
if i > 1000 {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
x0 = x1;
|
||||
y0 = y1;
|
||||
|
||||
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||
|
||||
x1 = (self.to - y0 + m * x0) / m;
|
||||
y1 = self.oscillate(x1);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Duration::from_secs_f64(x1)
|
||||
}
|
||||
|
||||
/// Returns the spring position at a given time in seconds.
|
||||
fn oscillate(&self, t: f64) -> f64 {
|
||||
let b = self.params.damping;
|
||||
let m = self.params.mass;
|
||||
let k = self.params.stiffness;
|
||||
let v0 = self.initial_velocity;
|
||||
|
||||
let beta = b / (2. * m);
|
||||
let omega0 = (k / m).sqrt();
|
||||
|
||||
let x0 = self.from - self.to;
|
||||
|
||||
let envelope = (-beta * t).exp();
|
||||
|
||||
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
|
||||
// for the differential equation m*ẍ+b*ẋ+kx = 0
|
||||
|
||||
// f64::EPSILON is too small for this specific comparison, so we use
|
||||
// f32::EPSILON even though it's doubles.
|
||||
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
|
||||
// Critically damped.
|
||||
self.to + envelope * (x0 + (beta * x0 + v0) * t)
|
||||
} else if beta < omega0 {
|
||||
// Underdamped.
|
||||
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
|
||||
|
||||
self.to
|
||||
+ envelope
|
||||
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
|
||||
} else {
|
||||
// Overdamped.
|
||||
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
|
||||
|
||||
self.to
|
||||
+ envelope
|
||||
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -10,7 +10,7 @@ use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::Niri;
|
||||
use crate::niri::Niri;
|
||||
|
||||
pub mod tty;
|
||||
pub use tty::Tty;
|
||||
@@ -98,7 +98,7 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||
@@ -138,7 +138,7 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_monitors_active(&self, active: bool) {
|
||||
pub fn set_monitors_active(&mut self, active: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||
Backend::Winit(_) => (),
|
||||
|
||||
+363
-175
@@ -1,6 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -10,7 +11,7 @@ use std::{io, mem};
|
||||
use anyhow::{anyhow, Context};
|
||||
use libc::dev_t;
|
||||
use niri_config::Config;
|
||||
use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::{Format, Fourcc};
|
||||
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
|
||||
@@ -20,7 +21,7 @@ use smithay::backend::drm::{
|
||||
use smithay::backend::egl::context::ContextPriority;
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
||||
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
@@ -28,7 +29,7 @@ use smithay::backend::session::libseat::LibSeatSession;
|
||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel};
|
||||
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
|
||||
use smithay::reexports::drm::control::{
|
||||
@@ -41,6 +42,9 @@ use smithay::reexports::wayland_protocols;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::DeviceFd;
|
||||
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
||||
use smithay_drm_extras::edid::EdidInfo;
|
||||
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
||||
@@ -48,10 +52,10 @@ use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
|
||||
use super::RenderResult;
|
||||
use crate::frame_clock::FrameClock;
|
||||
use crate::niri::{RedrawState, State};
|
||||
use crate::render_helpers::AsGlesRenderer;
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::shaders;
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::Niri;
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
|
||||
|
||||
@@ -60,7 +64,7 @@ pub struct Tty {
|
||||
session: LibSeatSession,
|
||||
udev_dispatcher: Dispatcher<'static, UdevBackend, State>,
|
||||
libinput: Libinput,
|
||||
gpu_manager: GpuManager<GbmGlesBackend<GlesRenderer>>,
|
||||
gpu_manager: GpuManager<GbmGlesBackend<GlesRenderer, DrmDeviceFd>>,
|
||||
// DRM node corresponding to the primary GPU. May or may not be the same as
|
||||
// primary_render_node.
|
||||
primary_node: DrmNode,
|
||||
@@ -71,31 +75,30 @@ pub struct Tty {
|
||||
// The dma-buf global corresponds to the output device (the primary GPU). It is only `Some()`
|
||||
// if we have a device corresponding to the primary GPU.
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
|
||||
// the primary GPU.
|
||||
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
|
||||
// The output config had changed, but the session is paused, so we need to update it on resume.
|
||||
update_output_config_on_resume: bool,
|
||||
// Whether the debug tinting is enabled.
|
||||
debug_tint: bool,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
|
||||
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
|
||||
pub type TtyRenderer<'render> = MultiRenderer<
|
||||
'render,
|
||||
'render,
|
||||
'alloc,
|
||||
GbmGlesBackend<GlesRenderer>,
|
||||
GbmGlesBackend<GlesRenderer>,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
>;
|
||||
|
||||
pub type TtyFrame<'render, 'alloc, 'frame> = MultiFrame<
|
||||
pub type TtyFrame<'render, 'frame> = MultiFrame<
|
||||
'render,
|
||||
'render,
|
||||
'alloc,
|
||||
'frame,
|
||||
GbmGlesBackend<GlesRenderer>,
|
||||
GbmGlesBackend<GlesRenderer>,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
>;
|
||||
|
||||
pub type TtyRendererError<'render, 'alloc> = <TtyRenderer<'render, 'alloc> as Renderer>::Error;
|
||||
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
|
||||
|
||||
type GbmDrmCompositor = DrmCompositor<
|
||||
GbmAllocator<DrmDeviceFd>,
|
||||
@@ -104,7 +107,7 @@ type GbmDrmCompositor = DrmCompositor<
|
||||
DrmDeviceFd,
|
||||
>;
|
||||
|
||||
struct OutputDevice {
|
||||
pub struct OutputDevice {
|
||||
token: RegistrationToken,
|
||||
render_node: DrmNode,
|
||||
drm_scanner: DrmScanner,
|
||||
@@ -113,6 +116,42 @@ struct OutputDevice {
|
||||
// See https://github.com/Smithay/smithay/issues/1102.
|
||||
drm: DrmDevice,
|
||||
gbm: GbmDevice<DrmDeviceFd>,
|
||||
|
||||
pub drm_lease_state: DrmLeaseState,
|
||||
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
|
||||
active_leases: Vec<DrmLease>,
|
||||
}
|
||||
|
||||
impl OutputDevice {
|
||||
pub fn lease_request(
|
||||
&self,
|
||||
request: DrmLeaseRequest,
|
||||
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||
let mut builder = DrmLeaseBuilder::new(&self.drm);
|
||||
for connector in request.connectors {
|
||||
let (_, crtc) = self
|
||||
.non_desktop_connectors
|
||||
.iter()
|
||||
.find(|(conn, _)| connector == *conn)
|
||||
.ok_or_else(|| {
|
||||
warn!("Attempted to lease connector that is not non-desktop");
|
||||
LeaseRejected::default()
|
||||
})?;
|
||||
builder.add_connector(connector);
|
||||
builder.add_crtc(*crtc);
|
||||
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
|
||||
builder.add_plane(planes.primary.handle);
|
||||
}
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn new_lease(&mut self, lease: DrmLease) {
|
||||
self.active_leases.push(lease);
|
||||
}
|
||||
|
||||
pub fn remove_lease(&mut self, lease_id: u32) {
|
||||
self.active_leases.retain(|l| l.id() != lease_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -142,11 +181,19 @@ pub struct SurfaceDmabufFeedback {
|
||||
}
|
||||
|
||||
impl Tty {
|
||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
|
||||
let (session, notifier) = LibSeatSession::new().unwrap();
|
||||
pub fn new(
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (session, notifier) = LibSeatSession::new().context(
|
||||
"Error creating a session. This might mean that you're trying to run niri on a TTY \
|
||||
that is already busy, for example if you're running this inside tmux that had been \
|
||||
originally started on a different TTY",
|
||||
)?;
|
||||
let seat_name = session.seat();
|
||||
|
||||
let udev_backend = UdevBackend::new(session.seat()).unwrap();
|
||||
let udev_backend =
|
||||
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
|
||||
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
|
||||
state.backend.tty().on_udev_event(&mut state.niri, event);
|
||||
});
|
||||
@@ -155,7 +202,9 @@ impl Tty {
|
||||
.unwrap();
|
||||
|
||||
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||
libinput.udev_assign_seat(&seat_name).unwrap();
|
||||
libinput
|
||||
.udev_assign_seat(&seat_name)
|
||||
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
||||
|
||||
let input_backend = LibinputInputBackend::new(libinput.clone());
|
||||
event_loop
|
||||
@@ -190,18 +239,23 @@ impl Tty {
|
||||
Ok(gles)
|
||||
};
|
||||
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
|
||||
let gpu_manager = GpuManager::new(api).unwrap();
|
||||
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
|
||||
|
||||
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
|
||||
.unwrap_or_else(|| {
|
||||
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
|
||||
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
|
||||
.ok_or(())
|
||||
.or_else(|()| {
|
||||
let primary_gpu_path = udev::primary_gpu(&seat_name)
|
||||
.context("error getting the primary GPU")?
|
||||
.context("couldn't find a GPU")?;
|
||||
let primary_node = DrmNode::from_path(primary_gpu_path)
|
||||
.context("error opening the primary GPU DRM node")?;
|
||||
let primary_render_node = primary_node
|
||||
.node_with_type(NodeType::Render)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(primary_node, primary_render_node)
|
||||
});
|
||||
.context("error getting the render node for the primary GPU")?
|
||||
.context("error getting the render node for the primary GPU")?;
|
||||
|
||||
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
|
||||
})?;
|
||||
|
||||
let mut node_path = String::new();
|
||||
if let Some(path) = primary_render_node.dev_path() {
|
||||
@@ -211,7 +265,7 @@ impl Tty {
|
||||
}
|
||||
info!("using as the render node: {}", node_path);
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
config,
|
||||
session,
|
||||
udev_dispatcher,
|
||||
@@ -221,10 +275,11 @@ impl Tty {
|
||||
primary_render_node,
|
||||
devices: HashMap::new(),
|
||||
dmabuf_global: None,
|
||||
primary_allocator: None,
|
||||
update_output_config_on_resume: false,
|
||||
debug_tint: false,
|
||||
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
|
||||
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
@@ -277,7 +332,7 @@ impl Tty {
|
||||
|
||||
self.libinput.suspend();
|
||||
|
||||
for device in self.devices.values() {
|
||||
for device in self.devices.values_mut() {
|
||||
device.drm.pause();
|
||||
}
|
||||
}
|
||||
@@ -285,7 +340,7 @@ impl Tty {
|
||||
debug!("resuming session");
|
||||
|
||||
if self.libinput.resume().is_err() {
|
||||
error!("error resuming libinput");
|
||||
warn!("error resuming libinput");
|
||||
}
|
||||
|
||||
let mut device_list = self
|
||||
@@ -320,47 +375,13 @@ impl Tty {
|
||||
device_list.remove(&node.dev_id());
|
||||
|
||||
// It hasn't been removed, update its state as usual.
|
||||
let device = &self.devices[&node];
|
||||
device.drm.activate();
|
||||
|
||||
// HACK: force reset the connectors to make resuming work across sleep.
|
||||
let device = &self.devices[&node];
|
||||
let crtcs: Vec<_> = device
|
||||
.drm_scanner
|
||||
.crtcs()
|
||||
.map(|(_conn, crtc)| crtc)
|
||||
.collect();
|
||||
for crtc in crtcs {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
}
|
||||
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
let _ = device.drm_scanner.scan_connectors(&device.drm);
|
||||
let crtcs: Vec<_> = device
|
||||
.drm_scanner
|
||||
.crtcs()
|
||||
.map(|(conn, crtc)| (conn.clone(), crtc))
|
||||
.collect();
|
||||
for (conn, crtc) in crtcs {
|
||||
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
if let Err(err) = device.drm.activate(true) {
|
||||
warn!("error activating DRM device: {err:?}");
|
||||
}
|
||||
|
||||
// // Refresh the connectors.
|
||||
// self.device_changed(node.dev_id(), niri);
|
||||
|
||||
// // Refresh the state on unchanged connectors.
|
||||
// let device = self.devices.get_mut(&node).unwrap();
|
||||
// for surface in device.surfaces.values_mut() {
|
||||
// let compositor = &mut surface.compositor;
|
||||
// if let Err(err) = compositor.surface().reset_state() {
|
||||
// warn!("error resetting DRM surface state: {err}");
|
||||
// }
|
||||
// compositor.reset_buffers();
|
||||
// }
|
||||
|
||||
// niri.queue_redraw_all();
|
||||
// Refresh the connectors.
|
||||
self.device_changed(node.dev_id(), niri);
|
||||
}
|
||||
|
||||
// Add new devices.
|
||||
@@ -370,7 +391,16 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
if self.update_output_config_on_resume {
|
||||
self.on_output_config_changed(niri);
|
||||
}
|
||||
|
||||
self.refresh_ipc_outputs();
|
||||
|
||||
niri.idle_notifier_state.notify_activity(&niri.seat);
|
||||
niri.monitors_active = true;
|
||||
self.set_monitors_active(true);
|
||||
niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,6 +414,8 @@ impl Tty {
|
||||
debug!("device added: {device_id} {path:?}");
|
||||
|
||||
let node = DrmNode::from_dev_id(device_id)?;
|
||||
let drm_lease_state = DrmLeaseState::new::<State>(&niri.display_handle, &node)
|
||||
.context("Couldn't create DrmLeaseState")?;
|
||||
|
||||
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK;
|
||||
let fd = self.session.open(path, open_flags)?;
|
||||
@@ -392,15 +424,9 @@ impl Tty {
|
||||
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
|
||||
let gbm = GbmDevice::new(device_fd)?;
|
||||
|
||||
let display = EGLDisplay::new(gbm.clone())?;
|
||||
let display = unsafe { EGLDisplay::new(gbm.clone())? };
|
||||
let egl_device = EGLDevice::device_for_display(&display)?;
|
||||
|
||||
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
|
||||
// same as the one we just created here, so when ours is dropped at the end of the scope,
|
||||
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
|
||||
// beforehand.
|
||||
drop(display);
|
||||
|
||||
let render_node = egl_device
|
||||
.try_get_render_node()?
|
||||
.context("no render node")?;
|
||||
@@ -419,6 +445,8 @@ impl Tty {
|
||||
|
||||
renderer.bind_wl_display(&niri.display_handle)?;
|
||||
|
||||
shaders::init(renderer.as_gles_renderer());
|
||||
|
||||
// Create the dmabuf global.
|
||||
let primary_formats = renderer.dmabuf_formats().collect::<HashSet<_>>();
|
||||
let default_feedback =
|
||||
@@ -433,11 +461,6 @@ impl Tty {
|
||||
);
|
||||
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
|
||||
|
||||
// Create the primary allocator.
|
||||
let primary_allocator =
|
||||
DmabufAllocator(GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING));
|
||||
assert!(self.primary_allocator.replace(primary_allocator).is_none());
|
||||
|
||||
// Update the dmabuf feedbacks for all surfaces.
|
||||
for device in self.devices.values_mut() {
|
||||
for surface in device.surfaces.values_mut() {
|
||||
@@ -467,7 +490,7 @@ impl Tty {
|
||||
let meta = meta.expect("VBlank events must have metadata");
|
||||
tty.on_vblank(&mut state.niri, node, crtc, meta);
|
||||
}
|
||||
DrmEvent::Error(error) => error!("DRM error: {error}"),
|
||||
DrmEvent::Error(error) => warn!("DRM error: {error}"),
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
@@ -479,6 +502,9 @@ impl Tty {
|
||||
gbm,
|
||||
drm_scanner: DrmScanner::new(),
|
||||
surfaces: HashMap::new(),
|
||||
drm_lease_state,
|
||||
active_leases: Vec::new(),
|
||||
non_desktop_connectors: HashSet::new(),
|
||||
};
|
||||
assert!(self.devices.insert(node, device).is_none());
|
||||
|
||||
@@ -549,7 +575,7 @@ impl Tty {
|
||||
match self.gpu_manager.single_renderer(&device.render_node) {
|
||||
Ok(mut renderer) => renderer.unbind_wl_display(),
|
||||
Err(err) => {
|
||||
error!("error creating renderer during device removal: {err}");
|
||||
warn!("error creating renderer during device removal: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,8 +596,6 @@ impl Tty {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.primary_allocator = None;
|
||||
|
||||
// Clear the dmabuf feedbacks for all surfaces.
|
||||
for device in self.devices.values_mut() {
|
||||
for surface in device.surfaces.values_mut() {
|
||||
@@ -600,6 +624,41 @@ impl Tty {
|
||||
);
|
||||
debug!("connecting connector: {output_name}");
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
let non_desktop = device
|
||||
.drm
|
||||
.get_properties(connector.handle())
|
||||
.ok()
|
||||
.and_then(|props| {
|
||||
let (info, value) = props
|
||||
.into_iter()
|
||||
.filter_map(|(handle, value)| {
|
||||
let info = device.drm.get_property(handle).ok()?;
|
||||
Some((info, value))
|
||||
})
|
||||
.find(|(info, _)| info.name().to_str() == Ok("non-desktop"))?;
|
||||
|
||||
info.value_type().convert_value(value).as_boolean()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if non_desktop {
|
||||
debug!("output is non desktop");
|
||||
let description = get_edid_info(&device.drm, connector.handle())
|
||||
.map(|info| truncate_to_nul(info.model))
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
device.drm_lease_state.add_connector::<State>(
|
||||
connector.handle(),
|
||||
output_name,
|
||||
description,
|
||||
);
|
||||
device
|
||||
.non_desktop_connectors
|
||||
.insert((connector.handle(), crtc));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -614,8 +673,6 @@ impl Tty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
for m in connector.modes() {
|
||||
trace!("{m:?}");
|
||||
}
|
||||
@@ -648,15 +705,20 @@ impl Tty {
|
||||
// Update the output mode.
|
||||
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
|
||||
|
||||
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
|
||||
.map(|info| (info.manufacturer, info.model))
|
||||
let (make, model) = get_edid_info(&device.drm, connector.handle())
|
||||
.map(|info| {
|
||||
(
|
||||
truncate_to_nul(info.manufacturer),
|
||||
truncate_to_nul(info.model),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
||||
|
||||
let output = Output::new(
|
||||
output_name.clone(),
|
||||
PhysicalProperties {
|
||||
size: (physical_width as i32, physical_height as i32).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
subpixel: connector.subpixel().into(),
|
||||
model,
|
||||
make,
|
||||
},
|
||||
@@ -692,7 +754,7 @@ impl Tty {
|
||||
let render_formats = egl_context.dmabuf_render_formats();
|
||||
|
||||
// Create the compositor.
|
||||
let compositor = DrmCompositor::new(
|
||||
let mut compositor = DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
surface,
|
||||
Some(planes),
|
||||
@@ -705,6 +767,9 @@ impl Tty {
|
||||
device.drm.cursor_size(),
|
||||
cursor_plane_gbm,
|
||||
)?;
|
||||
if self.debug_tint {
|
||||
compositor.set_debug_flags(DebugFlags::TINT);
|
||||
}
|
||||
|
||||
let mut dmabuf_feedback = None;
|
||||
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
|
||||
@@ -735,13 +800,8 @@ impl Tty {
|
||||
let sequence_delta_plot_name =
|
||||
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
|
||||
|
||||
self.enabled_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(output_name.clone(), output.clone());
|
||||
|
||||
let surface = Surface {
|
||||
name: output_name,
|
||||
name: output_name.clone(),
|
||||
compositor,
|
||||
dmabuf_feedback,
|
||||
vblank_frame: None,
|
||||
@@ -755,9 +815,16 @@ impl Tty {
|
||||
|
||||
niri.add_output(output.clone(), Some(refresh_interval(mode)));
|
||||
|
||||
self.enabled_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(output_name, output.clone());
|
||||
#[cfg(feature = "dbus")]
|
||||
niri.on_enabled_outputs_changed();
|
||||
|
||||
// Power on all monitors if necessary and queue a redraw on the new one.
|
||||
niri.event_loop.insert_idle(move |state| {
|
||||
state.niri.activate_monitors(&state.backend);
|
||||
state.niri.activate_monitors(&mut state.backend);
|
||||
state.niri.queue_redraw(output);
|
||||
});
|
||||
|
||||
@@ -794,6 +861,8 @@ impl Tty {
|
||||
};
|
||||
|
||||
self.enabled_outputs.lock().unwrap().remove(&surface.name);
|
||||
#[cfg(feature = "dbus")]
|
||||
niri.on_enabled_outputs_changed();
|
||||
}
|
||||
|
||||
fn on_vblank(
|
||||
@@ -834,6 +903,11 @@ impl Tty {
|
||||
Duration::ZERO
|
||||
}
|
||||
};
|
||||
let presentation_time = if niri.config.borrow().debug.emulate_zero_presentation_time {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
presentation_time
|
||||
};
|
||||
|
||||
let message = if presentation_time.is_zero() {
|
||||
format!("vblank on {name}, presentation time unknown")
|
||||
@@ -883,16 +957,17 @@ impl Tty {
|
||||
.unwrap_or(Duration::ZERO);
|
||||
// FIXME: ideally should be monotonically increasing for a surface.
|
||||
let seq = meta.sequence as u64;
|
||||
let flags = wp_presentation_feedback::Kind::Vsync
|
||||
| wp_presentation_feedback::Kind::HwClock
|
||||
let mut flags = wp_presentation_feedback::Kind::Vsync
|
||||
| wp_presentation_feedback::Kind::HwCompletion;
|
||||
|
||||
feedback.presented::<_, smithay::utils::Monotonic>(
|
||||
presentation_time,
|
||||
refresh,
|
||||
seq,
|
||||
flags,
|
||||
);
|
||||
let time = if presentation_time.is_zero() {
|
||||
now
|
||||
} else {
|
||||
flags.insert(wp_presentation_feedback::Kind::HwClock);
|
||||
presentation_time
|
||||
};
|
||||
|
||||
feedback.presented::<_, smithay::utils::Monotonic>(time, refresh, seq, flags);
|
||||
|
||||
if !presentation_time.is_zero() {
|
||||
let misprediction_s =
|
||||
@@ -905,19 +980,19 @@ impl Tty {
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(err) => {
|
||||
error!("error marking frame as submitted: {err}");
|
||||
warn!("error marking frame as submitted: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_sequence) = output_state.current_estimated_sequence {
|
||||
if let Some(last_sequence) = output_state.last_drm_sequence {
|
||||
let delta = meta.sequence as f64 - last_sequence as f64;
|
||||
tracy_client::Client::running()
|
||||
.unwrap()
|
||||
.plot(surface.sequence_delta_plot_name, delta);
|
||||
}
|
||||
output_state.last_drm_sequence = Some(meta.sequence);
|
||||
|
||||
output_state.frame_clock.presented(presentation_time);
|
||||
output_state.current_estimated_sequence = Some(meta.sequence);
|
||||
|
||||
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
@@ -950,6 +1025,9 @@ impl Tty {
|
||||
return;
|
||||
};
|
||||
|
||||
// We waited for the timer, now we can send frame callbacks again.
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
RedrawState::Queued(_) => unreachable!(),
|
||||
@@ -962,14 +1040,10 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sequence) = output_state.current_estimated_sequence.as_mut() {
|
||||
*sequence = sequence.wrapping_add(1);
|
||||
|
||||
if output_state.unfinished_animations_remain {
|
||||
niri.queue_redraw(output);
|
||||
} else {
|
||||
niri.send_frame_callbacks(&output);
|
||||
}
|
||||
if output_state.unfinished_animations_remain {
|
||||
niri.queue_redraw(output);
|
||||
} else {
|
||||
niri.send_frame_callbacks(&output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,20 +1090,14 @@ impl Tty {
|
||||
return rv;
|
||||
}
|
||||
|
||||
let Some(allocator) = self.primary_allocator.as_mut() else {
|
||||
warn!("no primary allocator");
|
||||
return rv;
|
||||
};
|
||||
|
||||
let mut renderer = match self.gpu_manager.renderer(
|
||||
&self.primary_render_node,
|
||||
&device.render_node,
|
||||
allocator,
|
||||
surface.compositor.format(),
|
||||
) {
|
||||
Ok(renderer) => renderer,
|
||||
Err(err) => {
|
||||
error!("error creating renderer for primary GPU: {err:?}");
|
||||
warn!("error creating renderer for primary GPU: {err:?}");
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
@@ -1039,7 +1107,7 @@ impl Tty {
|
||||
|
||||
// Hand them over to the DRM.
|
||||
let drm_compositor = &mut surface.compositor;
|
||||
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
|
||||
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
|
||||
Ok(res) => {
|
||||
if self
|
||||
.config
|
||||
@@ -1058,7 +1126,7 @@ impl Tty {
|
||||
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
|
||||
}
|
||||
|
||||
if res.damage.is_some() {
|
||||
if !res.is_empty {
|
||||
let presentation_feedbacks =
|
||||
niri.take_presentation_feedbacks(output, &res.states);
|
||||
let data = (presentation_feedbacks, target_presentation_time);
|
||||
@@ -1079,10 +1147,16 @@ impl Tty {
|
||||
}
|
||||
};
|
||||
|
||||
// We queued this frame successfully, so the current client buffers were
|
||||
// latched. We can send frame callbacks now, since a new client commit
|
||||
// will no longer overwrite this frame and will wait for a VBlank.
|
||||
output_state.frame_callback_sequence =
|
||||
output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
return RenderResult::Submitted;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("error queueing frame: {err}");
|
||||
warn!("error queueing frame: {err}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1091,7 +1165,7 @@ impl Tty {
|
||||
}
|
||||
Err(err) => {
|
||||
// Can fail if we switched to a different TTY.
|
||||
error!("error rendering frame: {err}");
|
||||
warn!("error rendering frame: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,7 +1180,7 @@ impl Tty {
|
||||
|
||||
pub fn change_vt(&mut self, vt: i32) {
|
||||
if let Err(err) = self.session.change_vt(vt) {
|
||||
error!("error changing VT: {err}");
|
||||
warn!("error changing VT: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,36 +1192,42 @@ impl Tty {
|
||||
}
|
||||
|
||||
pub fn toggle_debug_tint(&mut self) {
|
||||
self.debug_tint = !self.debug_tint;
|
||||
|
||||
for device in self.devices.values_mut() {
|
||||
for surface in device.surfaces.values_mut() {
|
||||
let compositor = &mut surface.compositor;
|
||||
compositor.set_debug_flags(compositor.debug_flags() ^ DebugFlags::TINT);
|
||||
|
||||
let mut flags = compositor.debug_flags();
|
||||
flags.set(DebugFlags::TINT, self.debug_tint);
|
||||
compositor.set_debug_flags(flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) {
|
||||
Ok(renderer) => renderer,
|
||||
Err(err) => {
|
||||
debug!("error creating renderer for primary GPU: {err:?}");
|
||||
return Err(());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match renderer.import_dmabuf(dmabuf, None) {
|
||||
Ok(_texture) => Ok(()),
|
||||
Ok(_texture) => {
|
||||
dmabuf.set_node(Some(self.primary_render_node));
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("error importing dmabuf: {err:?}");
|
||||
Err(())
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn early_import(&mut self, surface: &WlSurface) {
|
||||
if let Err(err) = self.gpu_manager.early_import(
|
||||
// We always advertise the primary GPU in dmabuf feedback.
|
||||
Some(self.primary_render_node),
|
||||
// We always render on the primary GPU.
|
||||
self.primary_render_node,
|
||||
surface,
|
||||
@@ -1171,38 +1251,56 @@ impl Tty {
|
||||
|
||||
let physical_size = connector.size();
|
||||
|
||||
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
|
||||
.map(|info| (info.manufacturer, info.model))
|
||||
let (make, model) = get_edid_info(&device.drm, connector.handle())
|
||||
.map(|info| {
|
||||
(
|
||||
truncate_to_nul(info.manufacturer),
|
||||
truncate_to_nul(info.model),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
||||
|
||||
let surface = device.surfaces.get(&crtc);
|
||||
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
||||
let mut current_mode = None;
|
||||
|
||||
let modes = connector
|
||||
.modes()
|
||||
.iter()
|
||||
.map(|m| niri_ipc::Mode {
|
||||
width: m.size().0,
|
||||
height: m.size().1,
|
||||
refresh_rate: Mode::from(*m).refresh as u32,
|
||||
.filter(|m| !m.flags().contains(ModeFlags::INTERLACE))
|
||||
.enumerate()
|
||||
.map(|(idx, m)| {
|
||||
if Some(*m) == current_crtc_mode {
|
||||
current_mode = Some(idx);
|
||||
}
|
||||
|
||||
niri_ipc::Mode {
|
||||
width: m.size().0,
|
||||
height: m.size().1,
|
||||
refresh_rate: Mode::from(*m).refresh as u32,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut output = niri_ipc::Output {
|
||||
if let Some(crtc_mode) = current_crtc_mode {
|
||||
if current_mode.is_none() {
|
||||
if crtc_mode.flags().contains(ModeFlags::INTERLACE) {
|
||||
warn!("connector mode list missing current mode (interlaced)");
|
||||
} else {
|
||||
error!("connector mode list missing current mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = niri_ipc::Output {
|
||||
name: connector_name.clone(),
|
||||
make,
|
||||
model,
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode: None,
|
||||
current_mode,
|
||||
};
|
||||
|
||||
if let Some(surface) = device.surfaces.get(&crtc) {
|
||||
let current = surface.compositor.pending_mode();
|
||||
if let Some(current) = connector.modes().iter().position(|m| *m == current) {
|
||||
output.current_mode = Some(current);
|
||||
} else {
|
||||
error!("connector mode list missing current mode");
|
||||
}
|
||||
}
|
||||
|
||||
ipc_outputs.insert(connector_name, output);
|
||||
}
|
||||
}
|
||||
@@ -1223,10 +1321,22 @@ impl Tty {
|
||||
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
|
||||
}
|
||||
|
||||
pub fn set_monitors_active(&self, active: bool) {
|
||||
for device in self.devices.values() {
|
||||
for crtc in device.surfaces.keys() {
|
||||
set_crtc_active(&device.drm, *crtc, active);
|
||||
pub fn set_monitors_active(&mut self, active: bool) {
|
||||
// We only disable the CRTC here, this will also reset the
|
||||
// surface state so that the next call to `render_frame` will
|
||||
// always produce a new frame and `queue_frame` will change
|
||||
// the CRTC to active. This makes sure we always enable a CRTC
|
||||
// within an atomic operation.
|
||||
if active {
|
||||
return;
|
||||
}
|
||||
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1234,6 +1344,13 @@ impl Tty {
|
||||
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::on_output_config_changed");
|
||||
|
||||
// If we're inactive, we can't do anything, so just set a flag for later.
|
||||
if !self.session.is_active() {
|
||||
self.update_output_config_on_resume = true;
|
||||
return;
|
||||
}
|
||||
self.update_output_config_on_resume = false;
|
||||
|
||||
let mut to_disconnect = vec![];
|
||||
let mut to_connect = vec![];
|
||||
|
||||
@@ -1255,23 +1372,22 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if we need to change the mode.
|
||||
let connector = surface
|
||||
.compositor
|
||||
.current_connectors()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
|
||||
else {
|
||||
error!("surface pending connectors is empty");
|
||||
continue;
|
||||
};
|
||||
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
|
||||
error!("missing enabled connector in drm_scanner");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((mode, fallback)) = pick_mode(connector, config.mode) else {
|
||||
error!("couldn't pick mode for enabled connector");
|
||||
warn!("couldn't pick mode for enabled connector");
|
||||
continue;
|
||||
};
|
||||
|
||||
if surface.compositor.current_mode() == mode {
|
||||
if surface.compositor.pending_mode() == mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1365,6 +1481,10 @@ impl Tty {
|
||||
|
||||
self.refresh_ipc_outputs();
|
||||
}
|
||||
|
||||
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
||||
self.devices.get_mut(&node)
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
|
||||
@@ -1504,9 +1624,17 @@ fn refresh_interval(mode: DrmMode) -> Duration {
|
||||
#[cfg(feature = "dbus")]
|
||||
fn suspend() -> anyhow::Result<()> {
|
||||
let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?;
|
||||
let manager = logind_zbus::manager::ManagerProxyBlocking::new(&conn)
|
||||
.context("error creating login manager proxy")?;
|
||||
manager.suspend(true).context("error suspending")
|
||||
|
||||
conn.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Suspend",
|
||||
&(true),
|
||||
)
|
||||
.context("error suspending")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn queue_estimated_vblank_timer(
|
||||
@@ -1527,7 +1655,22 @@ fn queue_estimated_vblank_timer(
|
||||
}
|
||||
|
||||
let now = get_monotonic_time();
|
||||
let timer = Timer::from_duration(target_presentation_time.saturating_sub(now));
|
||||
let mut duration = target_presentation_time.saturating_sub(now);
|
||||
|
||||
// No use setting a zero timer, since we'll send frame callbacks anyway right after the call to
|
||||
// render(). This can happen for example with unknown presentation time from DRM.
|
||||
if duration.is_zero() {
|
||||
duration += output_state
|
||||
.frame_clock
|
||||
.refresh_interval()
|
||||
// Unknown refresh interval, i.e. winit backend. Would be good to estimate it somehow
|
||||
// but it's not that important for this code path.
|
||||
.unwrap_or(Duration::from_micros(16_667));
|
||||
}
|
||||
|
||||
trace!("queueing estimated vblank timer to fire in {duration:?}");
|
||||
|
||||
let timer = Timer::from_duration(duration);
|
||||
let token = niri
|
||||
.event_loop
|
||||
.insert_source(timer, move |_, _, data| {
|
||||
@@ -1555,6 +1698,11 @@ fn pick_mode(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Interlaced modes don't appear to work.
|
||||
if m.flags().contains(ModeFlags::INTERLACE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(refresh) = refresh {
|
||||
// If refresh is set, only pick modes with matching refresh.
|
||||
let wl_mode = Mode::from(*m);
|
||||
@@ -1600,3 +1748,43 @@ fn pick_mode(
|
||||
|
||||
mode.map(|m| (*m, fallback))
|
||||
}
|
||||
|
||||
fn truncate_to_nul(mut s: String) -> String {
|
||||
if let Some(index) = s.find('\0') {
|
||||
s.truncate(index);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<EdidInfo> {
|
||||
match catch_unwind(AssertUnwindSafe(move || {
|
||||
EdidInfo::for_connector(device, connector)
|
||||
})) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
warn!("edid-rs panicked: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn check(input: &str, expected: &str) {
|
||||
let input = String::from(input);
|
||||
assert_eq!(truncate_to_nul(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_to_nul_works() {
|
||||
check("", "");
|
||||
check("qwer", "qwer");
|
||||
check("abc\0def", "abc");
|
||||
check("\0as", "");
|
||||
check("a\0\0\0b", "a");
|
||||
check("bb😁\0cc", "bb😁");
|
||||
}
|
||||
}
|
||||
|
||||
+22
-20
@@ -16,12 +16,11 @@ 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::WindowBuilder;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use super::RenderResult;
|
||||
use crate::niri::{RedrawState, State};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::shaders;
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::Niri;
|
||||
|
||||
pub struct Winit {
|
||||
config: Rc<RefCell<Config>>,
|
||||
@@ -33,12 +32,15 @@ pub struct Winit {
|
||||
}
|
||||
|
||||
impl Winit {
|
||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
|
||||
pub fn new(
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<State>,
|
||||
) -> Result<Self, winit::Error> {
|
||||
let builder = WindowBuilder::new()
|
||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||
// .with_resizable(false)
|
||||
.with_title("niri");
|
||||
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
||||
let (backend, winit) = winit::init_from_builder(builder)?;
|
||||
|
||||
let output = Output::new(
|
||||
"winit".to_string(),
|
||||
@@ -54,7 +56,7 @@ impl Winit {
|
||||
size: backend.window_size(),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
@@ -107,32 +109,28 @@ impl Winit {
|
||||
WinitEvent::Redraw => state
|
||||
.niri
|
||||
.queue_redraw(state.backend.winit().output.clone()),
|
||||
WinitEvent::CloseRequested => {
|
||||
state.niri.stop_signal.stop();
|
||||
state.niri.remove_output(&state.backend.winit().output);
|
||||
}
|
||||
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
config,
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
ipc_outputs,
|
||||
enabled_outputs,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
if let Err(err) = self
|
||||
.backend
|
||||
.renderer()
|
||||
.bind_wl_display(&niri.display_handle)
|
||||
{
|
||||
let renderer = self.backend.renderer();
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
}
|
||||
|
||||
shaders::init(renderer);
|
||||
|
||||
niri.add_output(self.output.clone(), None);
|
||||
}
|
||||
|
||||
@@ -201,6 +199,10 @@ impl Winit {
|
||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||
}
|
||||
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
|
||||
// right away instead.
|
||||
if output_state.unfinished_animations_remain {
|
||||
self.backend.window().request_redraw();
|
||||
}
|
||||
@@ -213,12 +215,12 @@ impl Winit {
|
||||
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
||||
Ok(_texture) => Ok(()),
|
||||
Ok(_texture) => true,
|
||||
Err(err) => {
|
||||
debug!("error importing dmabuf: {err:?}");
|
||||
Err(())
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use niri_ipc::Action;
|
||||
|
||||
use crate::utils::version;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version = version(), about, long_about = None)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||
#[command(subcommand_help_heading = "Subcommands")]
|
||||
pub struct Cli {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Import environment globally to systemd and D-Bus, run D-Bus services.
|
||||
///
|
||||
/// Set this flag in a systemd service started by your display manager, or when running
|
||||
/// manually as your main compositor instance. Do not set when running as a nested window, or
|
||||
/// on a TTY as your non-main compositor instance, to avoid messing up the global environment.
|
||||
#[arg(long)]
|
||||
pub session: bool,
|
||||
/// Command to run upon compositor startup.
|
||||
#[arg(last = true)]
|
||||
pub command: Vec<OsString>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Option<Sub>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
msg: Msg,
|
||||
/// Format output as JSON.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Msg {
|
||||
/// List connected outputs.
|
||||
Outputs,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
action: Action,
|
||||
},
|
||||
}
|
||||
+6
-20
@@ -8,8 +8,7 @@ use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::texture::TextureBuffer;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||
@@ -224,7 +223,7 @@ pub enum RenderCursor {
|
||||
},
|
||||
}
|
||||
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CursorTextureCache {
|
||||
@@ -238,12 +237,11 @@ impl CursorTextureCache {
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
icon: CursorIcon,
|
||||
scale: i32,
|
||||
cursor: &XCursor,
|
||||
idx: usize,
|
||||
) -> Option<TextureBuffer<GlesTexture>> {
|
||||
) -> MemoryRenderBuffer {
|
||||
self.cache
|
||||
.borrow_mut()
|
||||
.entry((icon, scale))
|
||||
@@ -252,26 +250,14 @@ impl CursorTextureCache {
|
||||
.frames()
|
||||
.iter()
|
||||
.map(|frame| {
|
||||
let _span = tracy_client::span!("create TextureBuffer");
|
||||
|
||||
let buffer = TextureBuffer::from_memory(
|
||||
renderer,
|
||||
MemoryRenderBuffer::from_slice(
|
||||
&frame.pixels_rgba,
|
||||
Fourcc::Abgr8888,
|
||||
Fourcc::Argb8888,
|
||||
(frame.width as i32, frame.height as i32),
|
||||
false,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
|
||||
match buffer {
|
||||
Ok(x) => Some(x),
|
||||
Err(err) => {
|
||||
warn!("error creating a cursor texture: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})[idx]
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use anyhow::Context;
|
||||
use futures_util::StreamExt;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||
use zbus::zvariant::NoneValue;
|
||||
use zbus::{dbus_interface, MessageHeader, Task};
|
||||
|
||||
use super::Start;
|
||||
|
||||
pub struct ScreenSaver {
|
||||
is_inhibited: Arc<AtomicBool>,
|
||||
is_broken: Arc<AtomicBool>,
|
||||
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||
counter: u32,
|
||||
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
async fn inhibit(
|
||||
&mut self,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
application_name: &str,
|
||||
reason_for_inhibit: &str,
|
||||
) -> fdo::Result<u32> {
|
||||
trace!(
|
||||
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
|
||||
hdr.sender()
|
||||
);
|
||||
|
||||
let Ok(Some(name)) = hdr.sender() else {
|
||||
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||
};
|
||||
let name = OwnedUniqueName::from(name.to_owned());
|
||||
|
||||
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||
|
||||
let mut cookie = None;
|
||||
for _ in 0..3 {
|
||||
// Start from 1 because some clients don't like 0.
|
||||
self.counter = self.counter.wrapping_add(1);
|
||||
if self.counter == 0 {
|
||||
self.counter += 1;
|
||||
}
|
||||
|
||||
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
|
||||
entry.insert(name);
|
||||
self.is_inhibited.store(true, Ordering::SeqCst);
|
||||
cookie = Some(self.counter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
|
||||
}
|
||||
|
||||
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
|
||||
trace!("fdo uninhibit, cookie: {cookie}");
|
||||
|
||||
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||
|
||||
if inhibitors.remove(&cookie).is_some() {
|
||||
if inhibitors.is_empty() {
|
||||
self.is_inhibited.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(fdo::Error::Failed(String::from("invalid cookie")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenSaver {
|
||||
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
|
||||
Self {
|
||||
is_inhibited,
|
||||
is_broken: Arc::new(AtomicBool::new(false)),
|
||||
inhibitors: Arc::new(Mutex::new(HashMap::new())),
|
||||
counter: 0,
|
||||
monitor_task: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn monitor_disappeared_clients(
|
||||
conn: &zbus::Connection,
|
||||
is_inhibited: Arc<AtomicBool>,
|
||||
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let proxy = fdo::DBusProxy::new(conn)
|
||||
.await
|
||||
.context("error creating a DBusProxy")?;
|
||||
|
||||
let mut stream = proxy
|
||||
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
|
||||
.await
|
||||
.context("error creating a NameOwnerChanged stream")?;
|
||||
|
||||
while let Some(signal) = stream.next().await {
|
||||
let args = signal
|
||||
.args()
|
||||
.context("error retrieving NameOwnerChanged args")?;
|
||||
|
||||
let Some(name) = &**args.old_owner() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if args.new_owner().is_none() {
|
||||
trace!("fdo ScreenSaver client disappeared: {name}");
|
||||
|
||||
let mut inhibitors = inhibitors.lock().unwrap();
|
||||
inhibitors.retain(|_, owner| owner != name);
|
||||
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
|
||||
} else {
|
||||
error!("non-null new_owner should've been filtered out");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Start for ScreenSaver {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let is_inhibited = self.is_inhibited.clone();
|
||||
let is_broken = self.is_broken.clone();
|
||||
let inhibitors = self.inhibitors.clone();
|
||||
let monitor_task = self.monitor_task.clone();
|
||||
|
||||
let conn = zbus::blocking::Connection::session()?;
|
||||
let flags = RequestNameFlags::AllowReplacement
|
||||
| RequestNameFlags::ReplaceExisting
|
||||
| RequestNameFlags::DoNotQueue;
|
||||
|
||||
conn.object_server()
|
||||
.at("/org/freedesktop/ScreenSaver", self)?;
|
||||
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
|
||||
|
||||
let async_conn = conn.inner();
|
||||
let future = {
|
||||
let conn = async_conn.clone();
|
||||
async move {
|
||||
if let Err(err) =
|
||||
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
|
||||
.await
|
||||
{
|
||||
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
|
||||
is_broken.store(true, Ordering::SeqCst);
|
||||
is_inhibited.store(false, Ordering::SeqCst);
|
||||
inhibitors.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
let task = async_conn
|
||||
.executor()
|
||||
.spawn(future, "monitor disappearing clients");
|
||||
monitor_task.set(task).unwrap();
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::dbus_interface;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
|
||||
|
||||
+6
-1
@@ -1,9 +1,9 @@
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::Interface;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub mod freedesktop_screensaver;
|
||||
pub mod gnome_shell_screenshot;
|
||||
pub mod mutter_display_config;
|
||||
pub mod mutter_service_channel;
|
||||
@@ -13,6 +13,7 @@ pub mod mutter_screen_cast;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
use mutter_screen_cast::ScreenCast;
|
||||
|
||||
use self::freedesktop_screensaver::ScreenSaver;
|
||||
use self::mutter_display_config::DisplayConfig;
|
||||
use self::mutter_service_channel::ServiceChannel;
|
||||
|
||||
@@ -24,6 +25,7 @@ trait Start: Interface {
|
||||
pub struct DBusServers {
|
||||
pub conn_service_channel: Option<Connection>,
|
||||
pub conn_display_config: Option<Connection>,
|
||||
pub conn_screen_saver: Option<Connection>,
|
||||
pub conn_screen_shot: Option<Connection>,
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub conn_screen_cast: Option<Connection>,
|
||||
@@ -48,6 +50,9 @@ impl DBusServers {
|
||||
let display_config = DisplayConfig::new(backend.enabled_outputs());
|
||||
dbus.conn_display_config = try_start(display_config);
|
||||
|
||||
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||
dbus.conn_screen_saver = try_start(screen_saver);
|
||||
|
||||
let (to_niri, from_screenshot) = calloop::channel::channel();
|
||||
let (to_screenshot, from_niri) = async_channel::unbounded();
|
||||
niri.event_loop
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::Serialize;
|
||||
use smithay::output::Output;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
use zbus::{dbus_interface, fdo};
|
||||
use zbus::{dbus_interface, fdo, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -112,7 +112,8 @@ impl DisplayConfig {
|
||||
Ok((0, monitors, logical_monitors, HashMap::new()))
|
||||
}
|
||||
|
||||
// FIXME: monitors-changed signal.
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
|
||||
@@ -5,12 +5,12 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
use crate::utils::output_size;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenCast {
|
||||
@@ -54,6 +54,13 @@ pub struct Stream {
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct StreamParameters {
|
||||
/// Size of the stream in logical coordinates.
|
||||
size: (i32, i32),
|
||||
}
|
||||
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
@@ -195,6 +202,12 @@ impl Stream {
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(property)]
|
||||
async fn parameters(&self) -> StreamParameters {
|
||||
let size = output_size(&self.output).into();
|
||||
StreamParameters { size }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCast {
|
||||
|
||||
+77
-10
@@ -16,9 +16,9 @@ use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{delegate_compositor, delegate_shm};
|
||||
|
||||
use super::xdg_shell;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::clone2;
|
||||
use crate::window::{InitialConfigureState, Unmapped};
|
||||
|
||||
impl CompositorHandler for State {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
@@ -75,7 +75,7 @@ impl CompositorHandler for State {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
@@ -97,23 +97,81 @@ impl CompositorHandler for State {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if is_mapped {
|
||||
// The toplevel got mapped.
|
||||
let window = entry.remove();
|
||||
let Unmapped { window, state } = entry.remove();
|
||||
|
||||
window.on_commit();
|
||||
|
||||
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
|
||||
{
|
||||
let (width, is_full_width, output) =
|
||||
if let InitialConfigureState::Configured {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
// Check that the output is still connected.
|
||||
let output =
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
(width, is_full_width, output)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(None, false, None)
|
||||
};
|
||||
|
||||
let parent = window
|
||||
.toplevel()
|
||||
.expect("no x11 support")
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
// Only consider the parent if we configured the window for the same
|
||||
// output.
|
||||
//
|
||||
// Normally when we're following the parent, the configured output will be
|
||||
// None. If the configured output is set, that means it was set explicitly
|
||||
// by a window rule or a fullscreen request.
|
||||
.filter(|(_, parent_output)| {
|
||||
output.is_none() || output.as_ref() == Some(*parent_output)
|
||||
})
|
||||
.map(|(window, _)| window.clone());
|
||||
|
||||
let win = 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, win, width, is_full_width)
|
||||
} else if let Some(output) = &output {
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_on_output(output, win, width, is_full_width);
|
||||
Some(output)
|
||||
} else {
|
||||
self.niri.layout.add_window(win, width, is_full_width)
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
self.niri.layout.start_open_animation_for_window(&window);
|
||||
self.niri.queue_redraw(output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The toplevel remains unmapped.
|
||||
let window = entry.get();
|
||||
xdg_shell::send_initial_configure_if_needed(window.toplevel());
|
||||
let unmapped = entry.get();
|
||||
if unmapped.needs_initial_configure() {
|
||||
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
|
||||
self.queue_initial_configure(toplevel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,12 +183,21 @@ impl CompositorHandler for State {
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if !is_mapped {
|
||||
// The toplevel got unmapped.
|
||||
self.niri.layout.remove_window(&window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), window);
|
||||
|
||||
// Newly-unmapped toplevels must perform the initial commit-configure sequence
|
||||
// afresh.
|
||||
let unmapped = Unmapped::new(window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||
|
||||
self.niri.queue_redraw(output);
|
||||
return;
|
||||
}
|
||||
|
||||
+176
-13
@@ -9,10 +9,12 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -20,7 +22,13 @@ use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
@@ -39,18 +47,25 @@ use smithay::wayland::session_lock::{
|
||||
};
|
||||
use smithay::{
|
||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
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_virtual_keyboard_manager,
|
||||
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, 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,
|
||||
};
|
||||
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
|
||||
use crate::utils::output_size;
|
||||
use crate::{delegate_foreign_toplevel, delegate_screencopy};
|
||||
|
||||
impl SeatHandler for State {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
type TouchFocus = WlSurface;
|
||||
|
||||
fn seat_state(&mut self) -> &mut SeatState<State> {
|
||||
&mut self.niri.seat_state
|
||||
@@ -73,6 +88,19 @@ impl SeatHandler for State {
|
||||
set_data_device_focus(dh, seat, client.clone());
|
||||
set_primary_focus(dh, seat, client);
|
||||
}
|
||||
|
||||
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
|
||||
let keyboards = self
|
||||
.niri
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
|
||||
.cloned();
|
||||
|
||||
for mut keyboard in keyboards {
|
||||
keyboard.led_update(led_state.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_seat!(State);
|
||||
delegate_cursor_shape!(State);
|
||||
@@ -189,6 +217,11 @@ impl DataControlHandler for State {
|
||||
|
||||
delegate_data_control!(State);
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
||||
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
||||
}
|
||||
}
|
||||
delegate_output!(State);
|
||||
|
||||
delegate_presentation!(State);
|
||||
@@ -204,13 +237,10 @@ impl DmabufHandler for State {
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
match self.backend.import_dmabuf(&dmabuf) {
|
||||
Ok(_) => {
|
||||
let _ = notifier.successful::<State>();
|
||||
}
|
||||
Err(_) => {
|
||||
notifier.failed();
|
||||
}
|
||||
if self.backend.import_dmabuf(&dmabuf) {
|
||||
let _ = notifier.successful::<State>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +298,7 @@ impl SecurityContextHandler for State {
|
||||
});
|
||||
|
||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||
error!("error inserting client: {err}");
|
||||
warn!("error inserting client: {err}");
|
||||
} else {
|
||||
trace!("inserted a new restricted client, context={context:?}");
|
||||
}
|
||||
@@ -277,3 +307,136 @@ impl SecurityContextHandler for State {
|
||||
}
|
||||
}
|
||||
delegate_security_context!(State);
|
||||
|
||||
impl IdleNotifierHandler for State {
|
||||
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
||||
&mut self.niri.idle_notifier_state
|
||||
}
|
||||
}
|
||||
delegate_idle_notify!(State);
|
||||
|
||||
impl IdleInhibitHandler for State {
|
||||
fn inhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.insert(surface);
|
||||
}
|
||||
|
||||
fn uninhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.remove(&surface);
|
||||
}
|
||||
}
|
||||
delegate_idle_inhibit!(State);
|
||||
|
||||
impl ForeignToplevelHandler for State {
|
||||
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
|
||||
&mut self.niri.foreign_toplevel_state
|
||||
}
|
||||
|
||||
fn activate(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
window.toplevel().expect("no x11 support").send_close();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||
{
|
||||
if !window
|
||||
.toplevel()
|
||||
.expect("no x11 support")
|
||||
.current_state()
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let window = window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(window.clone(), &requested_output);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_foreign_toplevel!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, screencopy: Screencopy) {
|
||||
if let Err(err) = self
|
||||
.niri
|
||||
.render_for_screencopy(&mut self.backend, screencopy)
|
||||
{
|
||||
warn!("error rendering for screencopy: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_screencopy!(State);
|
||||
|
||||
impl DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
&mut self
|
||||
.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.drm_lease_state
|
||||
}
|
||||
|
||||
fn lease_request(
|
||||
&mut self,
|
||||
node: DrmNode,
|
||||
request: DrmLeaseRequest,
|
||||
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||
debug!(
|
||||
"Received lease request for {} connectors",
|
||||
request.connectors.len()
|
||||
);
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.lease_request(request)
|
||||
}
|
||||
|
||||
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
|
||||
debug!("Lease success");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.new_lease(lease);
|
||||
}
|
||||
|
||||
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
|
||||
debug!("Destroyed lease");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.remove_lease(lease_id);
|
||||
}
|
||||
}
|
||||
delegate_drm_lease!(State);
|
||||
|
||||
delegate_viewporter!(State);
|
||||
|
||||
+400
-57
@@ -1,3 +1,4 @@
|
||||
use niri_config::{Match, WindowRule};
|
||||
use smithay::desktop::{
|
||||
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||
@@ -13,17 +14,99 @@ use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
||||
XdgShellState, XdgToplevelSurfaceData,
|
||||
XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||
use smithay::{
|
||||
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
|
||||
};
|
||||
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
|
||||
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
use crate::niri::{PopupGrabState, State};
|
||||
use crate::utils::clone2;
|
||||
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
|
||||
|
||||
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
|
||||
if let Some(app_id_re) = &m.app_id {
|
||||
let Some(app_id) = &role.app_id else {
|
||||
return false;
|
||||
};
|
||||
if !app_id_re.is_match(app_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(title_re) = &m.title {
|
||||
let Some(title) = &role.title else {
|
||||
return false;
|
||||
};
|
||||
if !title_re.is_match(title) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn resolve_window_rules(
|
||||
rules: &[WindowRule],
|
||||
toplevel: &ToplevelSurface,
|
||||
) -> ResolvedWindowRules {
|
||||
let _span = tracy_client::span!("resolve_window_rules");
|
||||
|
||||
let mut resolved = ResolvedWindowRules::default();
|
||||
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
let mut open_on_output = None;
|
||||
|
||||
for rule in rules {
|
||||
if !(rule.matches.is_empty() || rule.matches.iter().any(|m| window_matches(&role, m))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.excludes.iter().any(|m| window_matches(&role, m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(x) = rule
|
||||
.default_column_width
|
||||
.as_ref()
|
||||
.map(|d| d.0.map(ColumnWidth::from))
|
||||
{
|
||||
resolved.default_width = Some(x);
|
||||
}
|
||||
|
||||
if let Some(x) = rule.open_on_output.as_deref() {
|
||||
open_on_output = Some(x);
|
||||
}
|
||||
|
||||
if let Some(x) = rule.open_maximized {
|
||||
resolved.open_maximized = Some(x);
|
||||
}
|
||||
|
||||
if let Some(x) = rule.open_fullscreen {
|
||||
resolved.open_fullscreen = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
|
||||
});
|
||||
|
||||
resolved
|
||||
}
|
||||
|
||||
impl XdgShellHandler for State {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
@@ -32,27 +115,8 @@ impl XdgShellHandler for State {
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
let wl_surface = surface.wl_surface().clone();
|
||||
let window = Window::new(surface);
|
||||
|
||||
// Tell the surface the preferred size and bounds for its likely output.
|
||||
if let Some(ws) = self.niri.layout.active_workspace() {
|
||||
ws.configure_new_window(&window);
|
||||
}
|
||||
|
||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
||||
// rid of the various client-side rounded corners also by using the tiled state.
|
||||
let config = self.niri.config.borrow();
|
||||
if config.prefer_no_csd {
|
||||
window.toplevel().with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::TiledLeft);
|
||||
state.states.set(xdg_toplevel::State::TiledRight);
|
||||
state.states.set(xdg_toplevel::State::TiledTop);
|
||||
state.states.set(xdg_toplevel::State::TiledBottom);
|
||||
});
|
||||
}
|
||||
|
||||
// At the moment of creation, xdg toplevels must have no buffer.
|
||||
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
|
||||
let unmapped = Unmapped::new(Window::new_wayland_window(surface));
|
||||
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
|
||||
assert!(existing.is_none());
|
||||
}
|
||||
|
||||
@@ -94,6 +158,15 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||
// replace each other.
|
||||
// FIXME: do this properly.
|
||||
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||
return;
|
||||
}
|
||||
|
||||
let popup = PopupKind::Xdg(surface);
|
||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||
return;
|
||||
@@ -140,7 +213,9 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
let layout_focus = self.niri.layout.focus();
|
||||
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
|
||||
if Some(&root)
|
||||
!= layout_focus.map(|win| win.toplevel().expect("no x11 support").wl_surface())
|
||||
{
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
@@ -185,9 +260,11 @@ impl XdgShellHandler for State {
|
||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||
// FIXME
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
surface.send_configure();
|
||||
// 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) {
|
||||
surface.send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
|
||||
@@ -196,46 +273,167 @@ impl XdgShellHandler for State {
|
||||
|
||||
fn fullscreen_request(
|
||||
&mut self,
|
||||
surface: ToplevelSurface,
|
||||
toplevel: ToplevelSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
) {
|
||||
if surface
|
||||
.current_state()
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||
|
||||
if let Some((window, current_output)) = self
|
||||
.niri
|
||||
.layout
|
||||
.find_window_and_output(toplevel.wl_surface())
|
||||
{
|
||||
if let Some((window, current_output)) = self
|
||||
.niri
|
||||
.layout
|
||||
.find_window_and_output(surface.wl_surface())
|
||||
{
|
||||
let window = window.clone();
|
||||
let window = window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(window.clone(), &requested_output);
|
||||
}
|
||||
if let Some(requested_output) = requested_output {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(window.clone(), &requested_output);
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
}
|
||||
}
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
surface.send_configure();
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
toplevel.send_configure();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
match &mut unmapped.state {
|
||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||
*wants_fullscreen = Some(requested_output);
|
||||
|
||||
// The required configure will be the initial configure.
|
||||
}
|
||||
InitialConfigureState::Configured { output, .. } => {
|
||||
// Figure out the monitor following a similar logic to initial configure.
|
||||
// FIXME: deduplicate.
|
||||
let mon = requested_output
|
||||
.as_ref()
|
||||
// If none requested, try currently configured output.
|
||||
.or(output.as_ref())
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, false))
|
||||
// If not, check if we have a parent with a monitor.
|
||||
.or_else(|| {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
// If not, fall back to the active monitor.
|
||||
.or_else(|| {
|
||||
self.niri
|
||||
.layout
|
||||
.active_monitor_ref()
|
||||
.map(|mon| (mon, false))
|
||||
});
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = mon
|
||||
.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace());
|
||||
|
||||
if let Some(ws) = ws {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
ws.configure_new_window(&unmapped.window, None);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("couldn't find the toplevel in fullscreen_request()");
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
|
||||
if let Some((window, _)) = self
|
||||
.niri
|
||||
.layout
|
||||
.find_window_and_output(surface.wl_surface())
|
||||
.find_window_and_output(toplevel.wl_surface())
|
||||
{
|
||||
let window = window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
toplevel.send_configure();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
match &mut unmapped.state {
|
||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||
*wants_fullscreen = None;
|
||||
|
||||
// The required configure will be the initial configure.
|
||||
}
|
||||
InitialConfigureState::Configured {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
..
|
||||
} => {
|
||||
// Figure out the monitor following a similar logic to initial configure.
|
||||
// FIXME: deduplicate.
|
||||
let mon = output
|
||||
.as_ref()
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, false))
|
||||
// If not, check if we have a parent with a monitor.
|
||||
.or_else(|| {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
// If not, fall back to the active monitor.
|
||||
.or_else(|| {
|
||||
self.niri
|
||||
.layout
|
||||
.active_monitor_ref()
|
||||
.map(|mon| (mon, false))
|
||||
});
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = mon
|
||||
.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace());
|
||||
|
||||
if let Some(ws) = ws {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
|
||||
let configure_width = if *is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
} else {
|
||||
*width
|
||||
};
|
||||
ws.configure_new_window(&unmapped.window, configure_width);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("couldn't find the toplevel in unfullscreen_request()");
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +469,14 @@ impl XdgShellHandler for State {
|
||||
self.niri.queue_redraw(output.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
|
||||
self.update_window_rules(&toplevel);
|
||||
}
|
||||
|
||||
fn title_changed(&mut self, toplevel: ToplevelSurface) {
|
||||
self.update_window_rules(&toplevel);
|
||||
}
|
||||
}
|
||||
|
||||
delegate_xdg_shell!(State);
|
||||
@@ -323,14 +529,14 @@ impl KdeDecorationHandler for State {
|
||||
&self.niri.kde_decoration_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_kde_decoration!(State);
|
||||
|
||||
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
|
||||
if !initial_configure_sent(toplevel) {
|
||||
toplevel.send_configure();
|
||||
impl XdgForeignHandler for State {
|
||||
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
|
||||
&mut self.niri.xdg_foreign_state
|
||||
}
|
||||
}
|
||||
delegate_xdg_foreign!(State);
|
||||
|
||||
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
@@ -345,6 +551,131 @@ fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
|
||||
let _span = tracy_client::span!("State::send_initial_configure");
|
||||
|
||||
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
|
||||
error!("window must be present in unmapped_windows in send_initial_configure()");
|
||||
return;
|
||||
};
|
||||
|
||||
let Unmapped { window, state } = unmapped;
|
||||
|
||||
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
|
||||
error!("window must not be already configured in send_initial_configure()");
|
||||
return;
|
||||
};
|
||||
|
||||
let config = self.niri.config.borrow();
|
||||
let rules = resolve_window_rules(&config.window_rules, toplevel);
|
||||
|
||||
// Pick the target monitor. First, check if we had an output set in the window rules.
|
||||
let mon = rules
|
||||
.open_on_output
|
||||
.as_deref()
|
||||
.and_then(|name| self.niri.output_by_name.get(name))
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o));
|
||||
|
||||
// If not, check if the window requested one for fullscreen.
|
||||
let mon = mon.or_else(|| {
|
||||
wants_fullscreen
|
||||
.as_ref()
|
||||
.and_then(|x| x.as_ref())
|
||||
// The monitor might not exist if the output was disconnected.
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
});
|
||||
|
||||
// If not, check if this is a dialog with a parent, to place it next to the parent.
|
||||
let mon = mon.map(|mon| (mon, false)).or_else(|| {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
});
|
||||
|
||||
// If not, use the active monitor.
|
||||
let mon = mon.or_else(|| {
|
||||
self.niri
|
||||
.layout
|
||||
.active_monitor_ref()
|
||||
.map(|mon| (mon, false))
|
||||
});
|
||||
|
||||
// If we're following the parent, don't set the target output, so that when the window is
|
||||
// mapped, it fetches the possibly changed parent's output again, and shows up there.
|
||||
let output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let mut width = None;
|
||||
let is_full_width = rules.open_maximized.unwrap_or(false);
|
||||
|
||||
// Tell the surface the preferred size and bounds for its likely output.
|
||||
let ws = mon
|
||||
.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace());
|
||||
|
||||
if let Some(ws) = ws {
|
||||
// Set a fullscreen state based on window request and window rule.
|
||||
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|
||||
|| rules.open_fullscreen == Some(true)
|
||||
{
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
}
|
||||
|
||||
width = ws.resolve_default_width(rules.default_width);
|
||||
|
||||
let configure_width = if is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
} else {
|
||||
width
|
||||
};
|
||||
ws.configure_new_window(window, configure_width);
|
||||
}
|
||||
|
||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
||||
// rid of the various client-side rounded corners also by using the tiled state.
|
||||
if config.prefer_no_csd {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::TiledLeft);
|
||||
state.states.set(xdg_toplevel::State::TiledRight);
|
||||
state.states.set(xdg_toplevel::State::TiledTop);
|
||||
state.states.set(xdg_toplevel::State::TiledBottom);
|
||||
});
|
||||
}
|
||||
|
||||
// Set the configured settings.
|
||||
*state = InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
};
|
||||
|
||||
toplevel.send_configure();
|
||||
}
|
||||
|
||||
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
|
||||
// Send the initial configure in an idle, in case the client sent some more info after the
|
||||
// initial commit.
|
||||
self.niri.event_loop.insert_idle(move |state| {
|
||||
if !toplevel.alive() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
|
||||
if unmapped.needs_initial_configure() {
|
||||
state.send_initial_configure(&toplevel);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Should be called on `WlSurface::commit`
|
||||
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
|
||||
self.niri.popups.commit(surface);
|
||||
@@ -451,7 +782,9 @@ impl State {
|
||||
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
||||
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
||||
|
||||
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
|
||||
for (popup, _) in PopupManager::popups_for_surface(
|
||||
window.toplevel().expect("no x11 support").wl_surface(),
|
||||
) {
|
||||
match popup {
|
||||
PopupKind::Xdg(ref popup) => {
|
||||
if popup.with_pending_state(|state| state.positioner.reactive) {
|
||||
@@ -465,6 +798,16 @@ impl State {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
|
||||
let resolve = || resolve_window_rules(&self.niri.config.borrow().window_rules, toplevel);
|
||||
|
||||
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
|
||||
*rules = resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unconstrain_with_padding(
|
||||
|
||||
+509
-49
@@ -1,13 +1,16 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Action, Binds, LayoutAction, Modifiers};
|
||||
use input::event::gesture::GestureEventCoordinates as _;
|
||||
use niri_config::{Action, Binds, Modifiers};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
|
||||
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
|
||||
InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent,
|
||||
PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
|
||||
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState,
|
||||
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
|
||||
};
|
||||
use smithay::backend::libinput::LibinputInputBackend;
|
||||
use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState};
|
||||
@@ -16,14 +19,15 @@ use smithay::input::pointer::{
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::input;
|
||||
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
|
||||
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::{center, get_monotonic_time, spawn};
|
||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::spawning::spawn;
|
||||
use crate::utils::{center, get_monotonic_time};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompositorMod {
|
||||
@@ -37,7 +41,7 @@ pub struct TabletData {
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>)
|
||||
pub fn process_input_event<I: InputBackend + 'static>(&mut self, event: InputEvent<I>)
|
||||
where
|
||||
I::Device: 'static, // Needed for downcasting.
|
||||
{
|
||||
@@ -49,9 +53,24 @@ impl State {
|
||||
// here.
|
||||
self.niri.layout.advance_animations(get_monotonic_time());
|
||||
|
||||
// Power on monitors if they were off.
|
||||
if should_activate_monitors(&event) {
|
||||
self.niri.activate_monitors(&self.backend);
|
||||
if self.niri.monitors_active {
|
||||
// Notify the idle-notifier of activity.
|
||||
if should_notify_activity(&event) {
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
} else {
|
||||
// Power on monitors if they were off.
|
||||
if should_activate_monitors(&event) {
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
|
||||
// Notify the idle-notifier of activity only if we're also powering on the
|
||||
// monitors.
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
}
|
||||
|
||||
let hide_hotkey_overlay =
|
||||
@@ -85,11 +104,12 @@ impl State {
|
||||
GesturePinchEnd { event } => self.on_gesture_pinch_end::<I>(event),
|
||||
GestureHoldBegin { event } => self.on_gesture_hold_begin::<I>(event),
|
||||
GestureHoldEnd { event } => self.on_gesture_hold_end::<I>(event),
|
||||
TouchDown { .. } => (),
|
||||
TouchMotion { .. } => (),
|
||||
TouchUp { .. } => (),
|
||||
TouchCancel { .. } => (),
|
||||
TouchFrame { .. } => (),
|
||||
TouchDown { event } => self.on_touch_down::<I>(event),
|
||||
TouchMotion { event } => self.on_touch_motion::<I>(event),
|
||||
TouchUp { event } => self.on_touch_up::<I>(event),
|
||||
TouchCancel { event } => self.on_touch_cancel::<I>(event),
|
||||
TouchFrame { event } => self.on_touch_frame::<I>(event),
|
||||
SwitchToggle { .. } => (),
|
||||
Special(_) => (),
|
||||
}
|
||||
|
||||
@@ -126,9 +146,25 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if device.has_capability(input::DeviceCapability::Keyboard) {
|
||||
if let Some(led_state) = self
|
||||
.niri
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.map(|keyboard| keyboard.led_state())
|
||||
{
|
||||
device.led_update(led_state.into());
|
||||
}
|
||||
}
|
||||
|
||||
if device.has_capability(input::DeviceCapability::Touch) {
|
||||
self.niri.touch.insert(device.clone());
|
||||
}
|
||||
|
||||
apply_libinput_settings(&self.niri.config.borrow().input, device);
|
||||
}
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
self.niri.touch.remove(device);
|
||||
self.niri.tablets.remove(device);
|
||||
self.niri.devices.remove(device);
|
||||
}
|
||||
@@ -143,6 +179,9 @@ impl State {
|
||||
let desc = TabletDescriptor::from(&device);
|
||||
tablet_seat.add_tablet::<Self>(&self.niri.display_handle, &desc);
|
||||
}
|
||||
if device.has_capability(DeviceCapability::Touch) && self.niri.seat.get_touch().is_none() {
|
||||
self.niri.seat.add_touch();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_device_removed(&mut self, device: impl Device) {
|
||||
@@ -157,6 +196,9 @@ impl State {
|
||||
tablet_seat.clear_tools();
|
||||
}
|
||||
}
|
||||
if device.has_capability(DeviceCapability::Touch) && self.niri.touch.is_empty() {
|
||||
self.niri.seat.remove_touch();
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the cursor position for the tablet event.
|
||||
@@ -220,6 +262,7 @@ impl State {
|
||||
|
||||
if let Some(dialog) = &this.niri.exit_confirm_dialog {
|
||||
if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
|
||||
info!("quitting after confirming exit dialog");
|
||||
this.niri.stop_signal.stop();
|
||||
}
|
||||
}
|
||||
@@ -246,24 +289,35 @@ impl State {
|
||||
return;
|
||||
}
|
||||
|
||||
self.do_action(action);
|
||||
}
|
||||
|
||||
pub fn do_action(&mut self, action: Action) {
|
||||
if self.niri.is_locked() && !allowed_when_locked(&action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(touch) = self.niri.seat.get_touch() {
|
||||
touch.cancel(self);
|
||||
}
|
||||
|
||||
match action {
|
||||
Action::Quit => {
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if dialog.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
Action::Quit(skip_confirmation) => {
|
||||
if !skip_confirmation {
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if dialog.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
info!("quitting because quit bind was pressed");
|
||||
self.niri.stop_signal.stop()
|
||||
}
|
||||
|
||||
info!("quitting as requested");
|
||||
self.niri.stop_signal.stop()
|
||||
}
|
||||
Action::ChangeVt(vt) => {
|
||||
self.backend.change_vt(vt);
|
||||
// Changing `VT` may not deliver the key releases, so clear the state.
|
||||
// Changing VT may not deliver the key releases, so clear the state.
|
||||
self.niri.suppressed_keys.clear();
|
||||
}
|
||||
Action::Suspend => {
|
||||
@@ -272,7 +326,7 @@ impl State {
|
||||
self.niri.suppressed_keys.clear();
|
||||
}
|
||||
Action::PowerOffMonitors => {
|
||||
self.niri.deactivate_monitors(&self.backend);
|
||||
self.niri.deactivate_monitors(&mut self.backend);
|
||||
}
|
||||
Action::ToggleDebugTint => {
|
||||
self.backend.toggle_debug_tint();
|
||||
@@ -335,7 +389,7 @@ impl State {
|
||||
}
|
||||
Action::CloseWindow => {
|
||||
if let Some(window) = self.niri.layout.focus() {
|
||||
window.toplevel().send_close();
|
||||
window.toplevel().expect("no x11 support").send_close();
|
||||
}
|
||||
}
|
||||
Action::FullscreenWindow => {
|
||||
@@ -350,8 +404,8 @@ impl State {
|
||||
self.niri.seat.get_keyboard().unwrap().with_xkb_state(
|
||||
self,
|
||||
|mut state| match action {
|
||||
LayoutAction::Next => state.cycle_next_layout(),
|
||||
LayoutAction::Prev => state.cycle_prev_layout(),
|
||||
LayoutSwitchTarget::Next => state.cycle_next_layout(),
|
||||
LayoutSwitchTarget::Prev => state.cycle_prev_layout(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -395,6 +449,16 @@ impl State {
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::ConsumeOrExpelWindowLeft => {
|
||||
self.niri.layout.consume_or_expel_window_left();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::ConsumeOrExpelWindowRight => {
|
||||
self.niri.layout.consume_or_expel_window_right();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::FocusColumnLeft => {
|
||||
self.niri.layout.focus_left();
|
||||
// FIXME: granular
|
||||
@@ -541,48 +605,56 @@ impl State {
|
||||
Action::MoveWindowToMonitorLeft => {
|
||||
if let Some(output) = self.niri.output_left() {
|
||||
self.niri.layout.move_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWindowToMonitorRight => {
|
||||
if let Some(output) = self.niri.output_right() {
|
||||
self.niri.layout.move_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWindowToMonitorDown => {
|
||||
if let Some(output) = self.niri.output_down() {
|
||||
self.niri.layout.move_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWindowToMonitorUp => {
|
||||
if let Some(output) = self.niri.output_up() {
|
||||
self.niri.layout.move_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveColumnToMonitorLeft => {
|
||||
if let Some(output) = self.niri.output_left() {
|
||||
self.niri.layout.move_column_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveColumnToMonitorRight => {
|
||||
if let Some(output) = self.niri.output_right() {
|
||||
self.niri.layout.move_column_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveColumnToMonitorDown => {
|
||||
if let Some(output) = self.niri.output_down() {
|
||||
self.niri.layout.move_column_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveColumnToMonitorUp => {
|
||||
if let Some(output) = self.niri.output_up() {
|
||||
self.niri.layout.move_column_to_output(&output);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
@@ -597,6 +669,30 @@ impl State {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorLeft => {
|
||||
if let Some(output) = self.niri.output_left() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorRight => {
|
||||
if let Some(output) = self.niri.output_right() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorDown => {
|
||||
if let Some(output) = self.niri.output_down() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorUp => {
|
||||
if let Some(output) = self.niri.output_up() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1091,13 +1187,7 @@ impl State {
|
||||
|
||||
fn on_gesture_swipe_begin<I: InputBackend>(&mut self, event: I::GestureSwipeBeginEvent) {
|
||||
if event.fingers() == 3 {
|
||||
if let Some(output) = self.niri.output_under_cursor() {
|
||||
self.niri.layout.workspace_switch_gesture_begin(&output);
|
||||
|
||||
// FIXME: granular. This one is awkward because this can cancel a gesture on
|
||||
// multiple other outputs in theory.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
self.niri.gesture_swipe_3f_cumulative = Some((0., 0.));
|
||||
|
||||
// We handled this event.
|
||||
return;
|
||||
@@ -1120,16 +1210,75 @@ impl State {
|
||||
);
|
||||
}
|
||||
|
||||
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
|
||||
fn on_gesture_swipe_update<I: InputBackend + 'static>(
|
||||
&mut self,
|
||||
event: I::GestureSwipeUpdateEvent,
|
||||
) where
|
||||
I::Device: 'static,
|
||||
{
|
||||
let mut delta_x = event.delta_x();
|
||||
let mut delta_y = event.delta_y();
|
||||
|
||||
if let Some(libinput_event) =
|
||||
(&event as &dyn Any).downcast_ref::<input::event::gesture::GestureSwipeUpdateEvent>()
|
||||
{
|
||||
delta_x = libinput_event.dx_unaccelerated();
|
||||
delta_y = libinput_event.dy_unaccelerated();
|
||||
}
|
||||
|
||||
let device = event.device();
|
||||
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
|
||||
if device.config_scroll_natural_scroll_enabled() {
|
||||
delta_x = -delta_x;
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
|
||||
*cx += delta_x;
|
||||
*cy += delta_y;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from GNOME Shell.
|
||||
let (cx, cy) = (*cx, *cy);
|
||||
if cx * cx + cy * cy >= 16. * 16. {
|
||||
self.niri.gesture_swipe_3f_cumulative = None;
|
||||
|
||||
if let Some(output) = self.niri.output_under_cursor() {
|
||||
if cx.abs() > cy.abs() {
|
||||
self.niri.layout.view_offset_gesture_begin(&output);
|
||||
} else {
|
||||
self.niri.layout.workspace_switch_gesture_begin(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp = Duration::from_micros(event.time());
|
||||
|
||||
let mut handled = false;
|
||||
let res = self
|
||||
.niri
|
||||
.layout
|
||||
.workspace_switch_gesture_update(event.delta_y());
|
||||
.workspace_switch_gesture_update(delta_y, timestamp);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(output);
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
|
||||
let res = self
|
||||
.niri
|
||||
.layout
|
||||
.view_offset_gesture_update(delta_x, timestamp);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(output);
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if handled {
|
||||
// We handled this event.
|
||||
return;
|
||||
}
|
||||
@@ -1150,13 +1299,25 @@ impl State {
|
||||
}
|
||||
|
||||
fn on_gesture_swipe_end<I: InputBackend>(&mut self, event: I::GestureSwipeEndEvent) {
|
||||
self.niri.gesture_swipe_3f_cumulative = None;
|
||||
|
||||
let mut handled = false;
|
||||
let res = self
|
||||
.niri
|
||||
.layout
|
||||
.workspace_switch_gesture_end(event.cancelled());
|
||||
if let Some(output) = res {
|
||||
self.niri.queue_redraw(output);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
let res = self.niri.layout.view_offset_gesture_end(event.cancelled());
|
||||
if let Some(output) = res {
|
||||
self.niri.queue_redraw(output);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if handled {
|
||||
// We handled this event.
|
||||
return;
|
||||
}
|
||||
@@ -1267,6 +1428,116 @@ impl State {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the cursor position for the touch event.
|
||||
///
|
||||
/// This function handles the touch output mapping, as well as coordinate transform
|
||||
fn compute_touch_location<I: InputBackend, E: AbsolutePositionEvent<I>>(
|
||||
&self,
|
||||
evt: &E,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
let output = self.niri.output_for_touch()?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let transform = output.current_transform();
|
||||
let size = transform.invert().transform_size(output_geo.size);
|
||||
Some(
|
||||
transform.transform_point_in(evt.position_transformed(size), &size.to_f64())
|
||||
+ output_geo.loc.to_f64(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_touch_down<I: InputBackend>(&mut self, evt: I::TouchDownEvent) {
|
||||
let Some(handle) = self.niri.seat.get_touch() else {
|
||||
return;
|
||||
};
|
||||
let Some(touch_location) = self.compute_touch_location(&evt) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !handle.is_grabbed() {
|
||||
let output_under_touch = self
|
||||
.niri
|
||||
.global_space
|
||||
.output_under(touch_location)
|
||||
.next()
|
||||
.cloned();
|
||||
if let Some(window) = self.niri.window_under(touch_location) {
|
||||
let window = window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
} else if let Some(output) = output_under_touch {
|
||||
self.niri.layout.activate_output(&output);
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
};
|
||||
};
|
||||
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let under = self
|
||||
.niri
|
||||
.surface_under_and_global_space(touch_location)
|
||||
.map(|under| under.surface);
|
||||
handle.down(
|
||||
self,
|
||||
under,
|
||||
&DownEvent {
|
||||
slot: evt.slot(),
|
||||
location: touch_location,
|
||||
serial,
|
||||
time: evt.time_msec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
fn on_touch_up<I: InputBackend>(&mut self, evt: I::TouchUpEvent) {
|
||||
let Some(handle) = self.niri.seat.get_touch() else {
|
||||
return;
|
||||
};
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
handle.up(
|
||||
self,
|
||||
&UpEvent {
|
||||
slot: evt.slot(),
|
||||
serial,
|
||||
time: evt.time_msec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
fn on_touch_motion<I: InputBackend>(&mut self, evt: I::TouchMotionEvent) {
|
||||
let Some(handle) = self.niri.seat.get_touch() else {
|
||||
return;
|
||||
};
|
||||
let Some(touch_location) = self.compute_touch_location(&evt) else {
|
||||
return;
|
||||
};
|
||||
let under = self
|
||||
.niri
|
||||
.surface_under_and_global_space(touch_location)
|
||||
.map(|under| under.surface);
|
||||
handle.motion(
|
||||
self,
|
||||
under,
|
||||
&TouchMotionEvent {
|
||||
slot: evt.slot(),
|
||||
location: touch_location,
|
||||
time: evt.time_msec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
fn on_touch_frame<I: InputBackend>(&mut self, _evt: I::TouchFrameEvent) {
|
||||
let Some(handle) = self.niri.seat.get_touch() else {
|
||||
return;
|
||||
};
|
||||
handle.frame(self);
|
||||
}
|
||||
fn on_touch_cancel<I: InputBackend>(&mut self, _evt: I::TouchCancelEvent) {
|
||||
let Some(handle) = self.niri.seat.get_touch() else {
|
||||
return;
|
||||
};
|
||||
handle.cancel(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
@@ -1351,6 +1622,15 @@ fn action(
|
||||
_ => (),
|
||||
}
|
||||
|
||||
bound_action(bindings, comp_mod, raw, mods)
|
||||
}
|
||||
|
||||
fn bound_action(
|
||||
bindings: &Binds,
|
||||
comp_mod: CompositorMod,
|
||||
raw: Option<Keysym>,
|
||||
mods: ModifiersState,
|
||||
) -> Option<Action> {
|
||||
// Handle configured binds.
|
||||
let mut modifiers = Modifiers::empty();
|
||||
if mods.ctrl {
|
||||
@@ -1366,14 +1646,12 @@ fn action(
|
||||
modifiers |= Modifiers::SUPER;
|
||||
}
|
||||
|
||||
let (mod_down, mut comp_mod) = match comp_mod {
|
||||
let (mod_down, comp_mod) = match comp_mod {
|
||||
CompositorMod::Super => (mods.logo, Modifiers::SUPER),
|
||||
CompositorMod::Alt => (mods.alt, Modifiers::ALT),
|
||||
};
|
||||
if mod_down {
|
||||
modifiers |= Modifiers::COMPOSITOR;
|
||||
} else {
|
||||
comp_mod = Modifiers::empty();
|
||||
}
|
||||
|
||||
let raw = raw?;
|
||||
@@ -1383,8 +1661,15 @@ fn action(
|
||||
continue;
|
||||
}
|
||||
|
||||
if bind.key.modifiers | comp_mod == modifiers {
|
||||
return bind.actions.first().cloned();
|
||||
let mut bind_modifiers = bind.key.modifiers;
|
||||
if bind_modifiers.contains(Modifiers::COMPOSITOR) {
|
||||
bind_modifiers |= comp_mod;
|
||||
} else if bind_modifiers.contains(comp_mod) {
|
||||
bind_modifiers |= Modifiers::COMPOSITOR;
|
||||
}
|
||||
|
||||
if bind_modifiers == modifiers {
|
||||
return Some(bind.action.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1394,9 +1679,9 @@ fn action(
|
||||
fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
match event {
|
||||
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
|
||||
InputEvent::PointerMotion { .. }
|
||||
| InputEvent::PointerMotionAbsolute { .. }
|
||||
| InputEvent::PointerButton { .. }
|
||||
| InputEvent::PointerAxis { .. }
|
||||
| InputEvent::GestureSwipeBegin { .. }
|
||||
| InputEvent::GesturePinchBegin { .. }
|
||||
@@ -1415,8 +1700,8 @@ fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
match event {
|
||||
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||
InputEvent::PointerButton { .. }
|
||||
| InputEvent::PointerAxis { .. }
|
||||
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
|
||||
InputEvent::PointerAxis { .. }
|
||||
| InputEvent::GestureSwipeBegin { .. }
|
||||
| InputEvent::GesturePinchBegin { .. }
|
||||
| InputEvent::TouchDown { .. }
|
||||
@@ -1430,8 +1715,8 @@ fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
match event {
|
||||
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||
InputEvent::PointerButton { .. }
|
||||
| InputEvent::PointerAxis { .. }
|
||||
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
|
||||
InputEvent::PointerAxis { .. }
|
||||
| InputEvent::GestureSwipeBegin { .. }
|
||||
| InputEvent::GesturePinchBegin { .. }
|
||||
| InputEvent::TouchDown { .. }
|
||||
@@ -1442,10 +1727,17 @@ fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bo
|
||||
}
|
||||
}
|
||||
|
||||
fn should_notify_activity<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
!matches!(
|
||||
event,
|
||||
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn allowed_when_locked(action: &Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
Action::Quit
|
||||
Action::Quit(_)
|
||||
| Action::ChangeVt(_)
|
||||
| Action::Suspend
|
||||
| Action::PowerOffMonitors
|
||||
@@ -1456,7 +1748,7 @@ fn allowed_when_locked(action: &Action) -> bool {
|
||||
fn allowed_during_screenshot(action: &Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
|
||||
Action::Quit(_) | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1467,6 +1759,7 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let c = &config.touchpad;
|
||||
let _ = device.config_tap_set_enabled(c.tap);
|
||||
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||
let _ = device.config_dwtp_set_enabled(c.dwtp);
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed);
|
||||
|
||||
@@ -1513,11 +1806,23 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let _ = device.config_accel_set_profile(default);
|
||||
}
|
||||
}
|
||||
|
||||
if is_trackpoint {
|
||||
let c = &config.trackpoint;
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed);
|
||||
|
||||
if let Some(accel_profile) = c.accel_profile {
|
||||
let _ = device.config_accel_set_profile(accel_profile.into());
|
||||
} else if let Some(default) = device.config_accel_default_profile() {
|
||||
let _ = device.config_accel_set_profile(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use niri_config::{Action, Bind, Binds, Key, Modifiers};
|
||||
use niri_config::{Bind, Key};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1529,7 +1834,7 @@ mod tests {
|
||||
keysym: close_keysym,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL,
|
||||
},
|
||||
actions: vec![Action::CloseWindow],
|
||||
action: Action::CloseWindow,
|
||||
}]);
|
||||
|
||||
let comp_mod = CompositorMod::Super;
|
||||
@@ -1642,4 +1947,159 @@ mod tests {
|
||||
// Ensure that no keys are being suppressed.
|
||||
assert!(suppressed_keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comp_mod_handling() {
|
||||
let bindings = Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::q,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::CloseWindow,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::h,
|
||||
modifiers: Modifiers::SUPER,
|
||||
},
|
||||
action: Action::FocusColumnLeft,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::j,
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
action: Action::FocusWindowDown,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::k,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
|
||||
},
|
||||
action: Action::FocusWindowUp,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::l,
|
||||
modifiers: Modifiers::SUPER | Modifiers::ALT,
|
||||
},
|
||||
action: Action::FocusColumnRight,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::CloseWindow)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnLeft)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
Some(Action::FocusWindowDown)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusWindowUp)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnRight)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-8
@@ -3,10 +3,10 @@ use std::io::{Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use niri_ipc::{Mode, Output, Request, Response};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_ipc::{Mode, Output, Reply, Request, Response};
|
||||
|
||||
use crate::Msg;
|
||||
use crate::cli::Msg;
|
||||
|
||||
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
||||
@@ -19,8 +19,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let mut stream =
|
||||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
||||
|
||||
let request = match msg {
|
||||
let request = match &msg {
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::Action { action } => Request::Action(action.clone()),
|
||||
};
|
||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||
stream
|
||||
@@ -35,12 +36,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
.read_to_end(&mut buf)
|
||||
.context("error reading IPC response")?;
|
||||
|
||||
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
|
||||
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
|
||||
|
||||
let response = reply
|
||||
.map_err(|msg| anyhow!(msg))
|
||||
.context("niri could not handle the request")?;
|
||||
|
||||
match msg {
|
||||
Msg::Outputs => {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let Response::Outputs(outputs) = response
|
||||
else {
|
||||
let Response::Outputs(outputs) = response else {
|
||||
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||
};
|
||||
|
||||
@@ -100,6 +104,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+23
-8
@@ -22,6 +22,7 @@ pub struct IpcServer {
|
||||
}
|
||||
|
||||
struct ClientCtx {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
};
|
||||
|
||||
let ctx = ClientCtx {
|
||||
event_loop: state.niri.event_loop.clone(),
|
||||
ipc_outputs: state.backend.ipc_outputs(),
|
||||
};
|
||||
|
||||
@@ -108,20 +110,33 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
|
||||
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
|
||||
let reply = process(&ctx, &buf).map_err(|err| {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
err.to_string()
|
||||
});
|
||||
|
||||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
|
||||
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
|
||||
|
||||
let response = match request {
|
||||
Request::Outputs => {
|
||||
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
|
||||
Response::Outputs(ipc_outputs)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.do_action(action);
|
||||
});
|
||||
Response::Handled
|
||||
}
|
||||
};
|
||||
|
||||
let buf = serde_json::to_vec(&response).context("error formatting response")?;
|
||||
write
|
||||
.write_all(&buf)
|
||||
.await
|
||||
.context("error writing response")?;
|
||||
|
||||
Ok(())
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
+99
-50
@@ -1,64 +1,72 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{self, Color};
|
||||
use niri_config::GradientRelativeTo;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::gradient::GradientRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FocusRing {
|
||||
buffers: [SolidColorBuffer; 4],
|
||||
locations: [Point<i32, Logical>; 4],
|
||||
is_off: bool,
|
||||
sizes: [Size<i32, Logical>; 4],
|
||||
full_size: Size<i32, Logical>,
|
||||
is_active: bool,
|
||||
is_border: bool,
|
||||
width: i32,
|
||||
active_color: Color,
|
||||
inactive_color: Color,
|
||||
config: niri_config::FocusRing,
|
||||
}
|
||||
|
||||
pub type FocusRingRenderElement = SolidColorRenderElement;
|
||||
niri_render_elements! {
|
||||
FocusRingRenderElement => {
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Gradient = GradientRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusRing {
|
||||
pub fn new(config: niri_config::FocusRing) -> Self {
|
||||
Self {
|
||||
buffers: Default::default(),
|
||||
locations: Default::default(),
|
||||
is_off: config.off,
|
||||
sizes: Default::default(),
|
||||
full_size: Default::default(),
|
||||
is_active: false,
|
||||
is_border: false,
|
||||
width: config.width.into(),
|
||||
active_color: config.active_color,
|
||||
inactive_color: config.inactive_color,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
||||
self.is_off = config.off;
|
||||
self.width = config.width.into();
|
||||
self.active_color = config.active_color;
|
||||
self.inactive_color = config.inactive_color;
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
win_pos: Point<i32, Logical>,
|
||||
win_size: Size<i32, Logical>,
|
||||
is_border: bool,
|
||||
) {
|
||||
if is_border {
|
||||
self.buffers[0].resize((win_size.w + self.width * 2, self.width));
|
||||
self.buffers[1].resize((win_size.w + self.width * 2, self.width));
|
||||
self.buffers[2].resize((self.width, win_size.h));
|
||||
self.buffers[3].resize((self.width, win_size.h));
|
||||
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
|
||||
let width = i32::from(self.config.width);
|
||||
self.full_size = win_size + Size::from((width * 2, width * 2));
|
||||
|
||||
self.locations[0] = win_pos + Point::from((-self.width, -self.width));
|
||||
self.locations[1] = win_pos + Point::from((-self.width, win_size.h));
|
||||
self.locations[2] = win_pos + Point::from((-self.width, 0));
|
||||
self.locations[3] = win_pos + Point::from((win_size.w, 0));
|
||||
if is_border {
|
||||
self.sizes[0] = Size::from((win_size.w + width * 2, width));
|
||||
self.sizes[1] = Size::from((win_size.w + width * 2, width));
|
||||
self.sizes[2] = Size::from((width, win_size.h));
|
||||
self.sizes[3] = Size::from((width, win_size.h));
|
||||
|
||||
for (buf, size) in zip(&mut self.buffers, self.sizes) {
|
||||
buf.resize(size);
|
||||
}
|
||||
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
self.locations[1] = Point::from((-width, win_size.h));
|
||||
self.locations[2] = Point::from((-width, 0));
|
||||
self.locations[3] = Point::from((win_size.w, 0));
|
||||
} else {
|
||||
let size = win_size + Size::from((self.width * 2, self.width * 2));
|
||||
self.buffers[0].resize(size);
|
||||
self.locations[0] = win_pos - Point::from((self.width, self.width));
|
||||
self.sizes[0] = self.full_size;
|
||||
self.buffers[0].resize(self.sizes[0]);
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
}
|
||||
|
||||
self.is_border = is_border;
|
||||
@@ -66,50 +74,91 @@ impl FocusRing {
|
||||
|
||||
pub fn set_active(&mut self, is_active: bool) {
|
||||
let color = if is_active {
|
||||
self.active_color.into()
|
||||
self.config.active_color.into()
|
||||
} else {
|
||||
self.inactive_color.into()
|
||||
self.config.inactive_color.into()
|
||||
};
|
||||
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color);
|
||||
}
|
||||
|
||||
self.is_active = is_active;
|
||||
}
|
||||
|
||||
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
view_size: Size<i32, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 4>::new();
|
||||
|
||||
if self.is_off {
|
||||
if self.config.off {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let mut push = |buffer, location: Point<i32, Logical>| {
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
let gradient = if self.is_active {
|
||||
self.config.active_gradient
|
||||
} else {
|
||||
self.config.inactive_gradient
|
||||
};
|
||||
|
||||
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
|
||||
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
|
||||
|
||||
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
|
||||
let elem = gradient.and_then(|gradient| {
|
||||
let gradient_area = match gradient.relative_to {
|
||||
GradientRelativeTo::Window => full_rect,
|
||||
GradientRelativeTo::WorkspaceView => view_rect,
|
||||
};
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
scale,
|
||||
Rectangle::from_loc_and_size(location, size),
|
||||
gradient_area,
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
)
|
||||
.map(Into::into)
|
||||
});
|
||||
|
||||
let elem = elem.unwrap_or_else(|| {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
});
|
||||
rv.push(elem);
|
||||
};
|
||||
|
||||
if self.is_border {
|
||||
for (buf, loc) in zip(&self.buffers, self.locations) {
|
||||
push(buf, loc);
|
||||
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
|
||||
push(buf, location + loc, size);
|
||||
}
|
||||
} else {
|
||||
push(&self.buffers[0], self.locations[0]);
|
||||
push(
|
||||
&self.buffers[0],
|
||||
location + self.locations[0],
|
||||
self.sizes[0],
|
||||
);
|
||||
}
|
||||
|
||||
rv.into_iter()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.width
|
||||
self.config.width.into()
|
||||
}
|
||||
|
||||
pub fn is_off(&self) -> bool {
|
||||
self.is_off
|
||||
self.config.off
|
||||
}
|
||||
}
|
||||
|
||||
+690
-121
File diff suppressed because it is too large
Load Diff
+195
-33
@@ -2,12 +2,10 @@ use std::cmp::min;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::SizeChange;
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
CropRenderElement, Relocate, RelocateRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use smithay::desktop::Window;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||
|
||||
@@ -16,8 +14,19 @@ use super::workspace::{
|
||||
};
|
||||
use super::{LayoutElement, Options};
|
||||
use crate::animation::Animation;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::swipe_tracker::SwipeTracker;
|
||||
use crate::utils::output_size;
|
||||
|
||||
/// Amount of touchpad movement to scroll the height of one workspace.
|
||||
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
|
||||
|
||||
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
||||
stiffness: 0.5,
|
||||
limit: 0.05,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Monitor<W: LayoutElement> {
|
||||
/// Output for this monitor.
|
||||
@@ -44,6 +53,7 @@ pub struct WorkspaceSwitchGesture {
|
||||
pub center_idx: usize,
|
||||
/// Current, fractional workspace index.
|
||||
pub current_idx: f64,
|
||||
pub tracker: SwipeTracker,
|
||||
}
|
||||
|
||||
pub type MonitorRenderElement<R> =
|
||||
@@ -77,6 +87,10 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_workspace_ref(&self) -> &Workspace<W> {
|
||||
&self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
|
||||
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
||||
&mut self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
@@ -86,6 +100,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: also compute and use current velocity.
|
||||
let current_idx = self
|
||||
.workspace_switch
|
||||
.as_ref()
|
||||
@@ -97,7 +112,9 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||
current_idx,
|
||||
idx as f64,
|
||||
Duration::from_millis(250),
|
||||
0.,
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -127,6 +144,26 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &W,
|
||||
window: W,
|
||||
width: ColumnWidth,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
let workspace_idx = self
|
||||
.workspaces
|
||||
.iter_mut()
|
||||
.position(|ws| ws.has_window(right_of))
|
||||
.unwrap();
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_window_right_of(right_of, window, width, is_full_width);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
@@ -216,6 +253,14 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_left(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_left();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_right(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_right();
|
||||
}
|
||||
|
||||
pub fn focus_left(&mut self) {
|
||||
self.active_workspace().focus_left();
|
||||
}
|
||||
@@ -547,14 +592,28 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let size = output_size(&self.output);
|
||||
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor() as usize;
|
||||
let after_idx = render_idx.ceil() as usize;
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
||||
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
|
||||
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
|
||||
(before_idx, Point::from((0, offset)))
|
||||
if before_idx < 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
(before_idx as usize, Point::from((0, offset)))
|
||||
} else {
|
||||
if after_idx >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(after_idx, Point::from((0, -size.h + offset)))
|
||||
};
|
||||
|
||||
@@ -578,16 +637,11 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.render_above_top_layer()
|
||||
}
|
||||
}
|
||||
|
||||
impl Monitor<Window> {
|
||||
pub fn render_elements<R: Renderer + ImportAll>(
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> Vec<MonitorRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
) -> Vec<MonitorRenderElement<R>> {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
|
||||
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
|
||||
@@ -598,37 +652,63 @@ impl Monitor<Window> {
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor() as usize;
|
||||
let after_idx = render_idx.ceil() as usize;
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
||||
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
let after = if after_idx < self.workspaces.len() {
|
||||
let after = self.workspaces[after_idx].render_elements(renderer);
|
||||
let after = after.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
output_scale,
|
||||
// HACK: crop to infinite bounds for all sides except the side
|
||||
// where the workspaces join,
|
||||
// otherwise it will cut pixel shaders and mess up
|
||||
// the coordinate space.
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, 0),
|
||||
(i32::MAX / 2, i32::MAX / 2),
|
||||
),
|
||||
)?,
|
||||
(0, -offset + size.h),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
|
||||
if before_idx < 0. {
|
||||
return after.collect();
|
||||
}
|
||||
|
||||
Some(after)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let before_idx = before_idx as usize;
|
||||
let before = self.workspaces[before_idx].render_elements(renderer);
|
||||
let after = self.workspaces[after_idx].render_elements(renderer);
|
||||
|
||||
let before = before.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
output_scale,
|
||||
Rectangle::from_extemities((0, offset), (size.w, size.h)),
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX / 2, size.h),
|
||||
),
|
||||
)?,
|
||||
(0, -offset),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
let after = after.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
output_scale,
|
||||
Rectangle::from_extemities((0, 0), (size.w, offset)),
|
||||
)?,
|
||||
(0, -offset + size.h),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
before.chain(after).collect()
|
||||
before.chain(after.into_iter().flatten()).collect()
|
||||
}
|
||||
None => {
|
||||
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
|
||||
@@ -656,4 +736,86 @@ impl Monitor<Window> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_begin(&mut self) {
|
||||
let center_idx = self.active_workspace_idx;
|
||||
let current_idx = self
|
||||
.workspace_switch
|
||||
.as_ref()
|
||||
.map(|s| s.current_idx())
|
||||
.unwrap_or(center_idx as f64);
|
||||
|
||||
let gesture = WorkspaceSwitchGesture {
|
||||
center_idx,
|
||||
current_idx,
|
||||
tracker: SwipeTracker::new(),
|
||||
};
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_update(
|
||||
&mut self,
|
||||
delta_y: f64,
|
||||
timestamp: Duration,
|
||||
) -> Option<bool> {
|
||||
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
||||
return None;
|
||||
};
|
||||
|
||||
gesture.tracker.push(delta_y, timestamp);
|
||||
|
||||
let pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
|
||||
let min = gesture.center_idx.saturating_sub(1) as f64;
|
||||
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
|
||||
let new_idx = gesture.center_idx as f64 + pos;
|
||||
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||
|
||||
if gesture.current_idx == new_idx {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
gesture.current_idx = new_idx;
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
|
||||
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if cancelled {
|
||||
self.workspace_switch = None;
|
||||
self.clean_up_workspaces();
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
|
||||
let min = gesture.center_idx.saturating_sub(1) as f64;
|
||||
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
|
||||
let new_idx = gesture.center_idx as f64 + pos;
|
||||
|
||||
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||
let new_idx = new_idx.round() as usize;
|
||||
|
||||
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
|
||||
min,
|
||||
max,
|
||||
gesture.center_idx as f64 + current_pos,
|
||||
);
|
||||
|
||||
self.active_workspace_idx = new_idx;
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||
gesture.current_idx,
|
||||
new_idx as f64,
|
||||
velocity,
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
+152
-43
@@ -3,14 +3,16 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use smithay::backend::renderer::element::utils::RescaleRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::focus_ring::FocusRing;
|
||||
use super::workspace::WorkspaceRenderElement;
|
||||
use super::{LayoutElement, Options};
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use super::{LayoutElement, LayoutElementRenderElement, Options};
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::offscreen::OffscreenRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
/// Toplevel window with decorations.
|
||||
#[derive(Debug)]
|
||||
@@ -21,6 +23,12 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The border around the window.
|
||||
border: FocusRing,
|
||||
|
||||
/// The focus ring around the window.
|
||||
///
|
||||
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
|
||||
/// currently here.
|
||||
focus_ring: FocusRing,
|
||||
|
||||
/// Whether this tile is fullscreen.
|
||||
///
|
||||
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||
@@ -33,24 +41,39 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The size we were requested to fullscreen into.
|
||||
fullscreen_size: Size<i32, Logical>,
|
||||
|
||||
/// The animation upon opening a window.
|
||||
open_animation: Option<Animation>,
|
||||
|
||||
/// Configurable properties of the layout.
|
||||
options: Rc<Options>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
TileRenderElement<R> => {
|
||||
LayoutElement = LayoutElementRenderElement<R>,
|
||||
FocusRing = FocusRingRenderElement,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Tile<W> {
|
||||
pub fn new(window: W, options: Rc<Options>) -> Self {
|
||||
Self {
|
||||
window,
|
||||
border: FocusRing::new(options.border),
|
||||
border: FocusRing::new(options.border.into()),
|
||||
focus_ring: FocusRing::new(options.focus_ring),
|
||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
||||
fullscreen_size: Default::default(),
|
||||
open_animation: None,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||
self.border.update_config(options.border);
|
||||
self.border.update_config(options.border.into());
|
||||
self.focus_ring.update_config(options.focus_ring);
|
||||
self.options = options;
|
||||
}
|
||||
|
||||
@@ -61,14 +84,37 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
|
||||
let width = self.border.width();
|
||||
self.border.update(
|
||||
(width, width).into(),
|
||||
self.window.size(),
|
||||
self.window.has_ssd(),
|
||||
);
|
||||
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||
self.border
|
||||
.update(self.window.size(), self.window.has_ssd());
|
||||
self.border.set_active(is_active);
|
||||
|
||||
self.focus_ring.update(self.tile_size(), self.has_ssd());
|
||||
self.focus_ring.set_active(is_active);
|
||||
|
||||
match &mut self.open_animation {
|
||||
Some(anim) => {
|
||||
anim.set_current_time(current_time);
|
||||
if anim.is_done() {
|
||||
self.open_animation = None;
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.open_animation.is_some()
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self) {
|
||||
self.open_animation = Some(Animation::new(
|
||||
0.,
|
||||
1.,
|
||||
0.,
|
||||
self.options.animations.window_open,
|
||||
niri_config::Animation::default_window_open(),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &W {
|
||||
@@ -141,6 +187,23 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.window.size()
|
||||
}
|
||||
|
||||
/// Returns an animated size of the tile for rendering and input.
|
||||
///
|
||||
/// During the window opening animation, windows to the right should gradually slide further to
|
||||
/// the right. This is what this visual size is used for. Other things like window resizes or
|
||||
/// transactions or new view position calculation always use the real size, instead of this
|
||||
/// visual size.
|
||||
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
|
||||
let size = self.tile_size();
|
||||
let v = self
|
||||
.open_animation
|
||||
.as_ref()
|
||||
.map(|anim| anim.value())
|
||||
.unwrap_or(1.)
|
||||
.max(0.);
|
||||
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
|
||||
}
|
||||
|
||||
pub fn buf_loc(&self) -> Point<i32, Logical> {
|
||||
let mut loc = Point::from((0, 0));
|
||||
loc += self.window_loc();
|
||||
@@ -232,46 +295,92 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.effective_border_width().is_some() || self.window.has_ssd()
|
||||
}
|
||||
|
||||
pub fn render<R: Renderer + ImportAll>(
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<WorkspaceRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
let mut rv = Vec::new();
|
||||
view_size: Size<i32, Logical>,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
let rv = self
|
||||
.window
|
||||
.render(renderer, location + self.window_loc(), scale)
|
||||
.into_iter()
|
||||
.map(Into::into);
|
||||
|
||||
let window_pos = location + self.window_loc();
|
||||
rv.extend(self.window.render(renderer, window_pos, scale));
|
||||
let elem = self.effective_border_width().map(|width| {
|
||||
self.border
|
||||
.render(
|
||||
renderer,
|
||||
location + Point::from((width, width)),
|
||||
scale,
|
||||
view_size,
|
||||
)
|
||||
.map(Into::into)
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
if self.effective_border_width().is_some() {
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(scale)
|
||||
.map(|elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
.map(Into::into),
|
||||
);
|
||||
}
|
||||
let elem = focus_ring.then(|| {
|
||||
self.focus_ring
|
||||
.render(renderer, location, scale, view_size)
|
||||
.map(Into::into)
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
if self.is_fullscreen {
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
let elem = self.is_fullscreen.then(|| {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&self.fullscreen_backdrop,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.push(elem.into());
|
||||
}
|
||||
)
|
||||
.into()
|
||||
});
|
||||
rv.chain(elem)
|
||||
}
|
||||
|
||||
rv
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
view_size: Size<i32, Logical>,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
if let Some(anim) = &self.open_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
|
||||
let elem = OffscreenRenderElement::new(
|
||||
renderer,
|
||||
scale.x as i32,
|
||||
&elements,
|
||||
anim.value().clamp(0., 1.) as f32,
|
||||
);
|
||||
self.window()
|
||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||
|
||||
let mut center = location;
|
||||
center.x += self.tile_size().w / 2;
|
||||
center.y += self.tile_size().h / 2;
|
||||
|
||||
Some(TileRenderElement::Offscreen(
|
||||
RescaleRenderElement::from_element(
|
||||
elem,
|
||||
center.to_physical_precise_round(scale),
|
||||
(anim.value() / 2. + 0.5).max(0.),
|
||||
),
|
||||
))
|
||||
.into_iter()
|
||||
.chain(None.into_iter().flatten())
|
||||
} else {
|
||||
self.window().set_offscreen_element_id(None);
|
||||
|
||||
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
|
||||
None.into_iter().chain(Some(elements).into_iter().flatten())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+557
-192
File diff suppressed because it is too large
Load Diff
+30
@@ -0,0 +1,30 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
pub mod animation;
|
||||
pub mod backend;
|
||||
pub mod cli;
|
||||
pub mod cursor;
|
||||
#[cfg(feature = "dbus")]
|
||||
pub mod dbus;
|
||||
pub mod frame_clock;
|
||||
pub mod handlers;
|
||||
pub mod input;
|
||||
pub mod ipc;
|
||||
pub mod layout;
|
||||
pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod rubber_band;
|
||||
pub mod swipe_tracker;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
+137
-108
@@ -1,95 +1,33 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
mod animation;
|
||||
mod backend;
|
||||
mod config_error_notification;
|
||||
mod cursor;
|
||||
#[cfg(feature = "dbus")]
|
||||
mod dbus;
|
||||
mod exit_confirm_dialog;
|
||||
mod frame_clock;
|
||||
mod handlers;
|
||||
mod hotkey_overlay;
|
||||
mod input;
|
||||
mod ipc;
|
||||
mod layout;
|
||||
mod niri;
|
||||
mod render_helpers;
|
||||
mod screenshot_ui;
|
||||
mod utils;
|
||||
mod watcher;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
mod pw_utils;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::{env, mem};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
use dummy_pw_utils as pw_utils;
|
||||
use git_version::git_version;
|
||||
use niri::{Niri, State};
|
||||
use niri::animation;
|
||||
use niri::cli::{Cli, Sub};
|
||||
#[cfg(feature = "dbus")]
|
||||
use niri::dbus;
|
||||
use niri::ipc::client::handle_msg;
|
||||
use niri::niri::State;
|
||||
use niri::utils::spawning::{
|
||||
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||
};
|
||||
use niri::utils::watcher::Watcher;
|
||||
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
|
||||
use niri_config::Config;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
use smithay::reexports::calloop::{self, EventLoop};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use utils::spawn;
|
||||
use watcher::Watcher;
|
||||
|
||||
use crate::ipc::client::handle_msg;
|
||||
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version = version(), about, long_about = None)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||
#[command(subcommand_help_heading = "Subcommands")]
|
||||
struct Cli {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Command to run upon compositor startup.
|
||||
#[arg(last = true)]
|
||||
command: Vec<OsString>,
|
||||
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<Sub>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
msg: Msg,
|
||||
/// Format output as JSON.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Msg {
|
||||
/// List connected outputs.
|
||||
Outputs,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
@@ -102,7 +40,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let is_systemd_service = env::var_os("NOTIFY_SOCKET").is_some();
|
||||
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
|
||||
|
||||
#[cfg(not(feature = "systemd"))]
|
||||
warn!(
|
||||
"running as a systemd service, but systemd support is compiled out. \
|
||||
Are you sure you did not forget to set `--features systemd`?"
|
||||
);
|
||||
}
|
||||
|
||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
@@ -111,21 +57,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
if is_systemd_service {
|
||||
// If we're starting as a systemd service, assume that the intention is to start on a TTY.
|
||||
// Remove DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will
|
||||
// cause the winit backend to be selected instead.
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.session {
|
||||
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
|
||||
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
|
||||
// the winit backend to be selected instead.
|
||||
if env::var_os("DISPLAY").is_some() {
|
||||
debug!("we're running as a systemd service but DISPLAY is set, removing it");
|
||||
warn!("running as a session but DISPLAY is set, removing it");
|
||||
env::remove_var("DISPLAY");
|
||||
}
|
||||
if env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||
debug!("we're running as a systemd service but WAYLAND_DISPLAY is set, removing it");
|
||||
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
|
||||
env::remove_var("WAYLAND_DISPLAY");
|
||||
}
|
||||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
// Set the current desktop for xdg-desktop-portal.
|
||||
env::set_var("XDG_CURRENT_DESKTOP", "niri");
|
||||
// Ensure the session type is set to Wayland for xdg-autostart and Qt apps.
|
||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||
}
|
||||
|
||||
let _client = tracy_client::Client::start();
|
||||
|
||||
@@ -154,7 +105,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("starting version {}", &version());
|
||||
|
||||
// Load the config.
|
||||
let path = cli.config.or_else(default_config_path);
|
||||
let mut config_created = false;
|
||||
let path = cli.config.or_else(|| {
|
||||
let default_path = default_config_path()?;
|
||||
let default_parent = default_path.parent().unwrap();
|
||||
|
||||
if let Err(err) = fs::create_dir_all(default_parent) {
|
||||
warn!(
|
||||
"error creating config directories {:?}: {err:?}",
|
||||
default_parent
|
||||
);
|
||||
return Some(default_path);
|
||||
}
|
||||
|
||||
// Create the config and fill it with the default config if it doesn't exist.
|
||||
let new_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&default_path);
|
||||
match new_file {
|
||||
Ok(mut new_file) => {
|
||||
let default = include_bytes!("../resources/default-config.kdl");
|
||||
match new_file.write_all(default) {
|
||||
Ok(()) => {
|
||||
config_created = true;
|
||||
info!("wrote default config to {:?}", &default_path);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error writing config file at {:?}: {err:?}", &default_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
|
||||
}
|
||||
|
||||
Some(default_path)
|
||||
});
|
||||
|
||||
let mut config_errored = false;
|
||||
let mut config = path
|
||||
@@ -169,8 +157,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
||||
let slowdown = if config.animations.off {
|
||||
0.
|
||||
} else {
|
||||
config.animations.slowdown.clamp(0., 100.)
|
||||
};
|
||||
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||
|
||||
// Create the compositor.
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
@@ -180,7 +175,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
event_loop.handle(),
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Set WAYLAND_DISPLAY for children.
|
||||
let socket_name = &state.niri.socket_name;
|
||||
@@ -196,9 +192,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||
}
|
||||
|
||||
if is_systemd_service {
|
||||
// We're starting as a systemd service. Export our variables.
|
||||
import_env_to_systemd();
|
||||
if cli.session {
|
||||
// We're starting as a session. Import our variables.
|
||||
import_environment();
|
||||
|
||||
// Inhibit power key handling so we can suspend on it.
|
||||
#[cfg(feature = "dbus")]
|
||||
@@ -210,15 +206,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
dbus::DBusServers::start(&mut state, is_systemd_service);
|
||||
dbus::DBusServers::start(&mut state, cli.session);
|
||||
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
|
||||
// Send ready notification to the NOTIFY_FD file descriptor.
|
||||
if let Err(err) = notify_fd() {
|
||||
warn!("error notifying fd: {err:?}");
|
||||
}
|
||||
|
||||
// Set up config file watcher.
|
||||
let _watcher = if let Some(path) = path {
|
||||
let _watcher = if let Some(path) = path.clone() {
|
||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||
let watcher = Watcher::new(path.clone(), tx);
|
||||
event_loop
|
||||
@@ -243,6 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Show the config error notification right away if needed.
|
||||
if config_errored {
|
||||
state.niri.config_error_notification.show();
|
||||
} else if config_created {
|
||||
state.niri.config_error_notification.show_created(path);
|
||||
}
|
||||
|
||||
// Run the compositor.
|
||||
@@ -253,21 +256,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
git_version!(fallback = "unknown commit"),
|
||||
)
|
||||
}
|
||||
fn import_environment() {
|
||||
let variables = [
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_CURRENT_DESKTOP",
|
||||
"XDG_SESSION_TYPE",
|
||||
niri_ipc::SOCKET_PATH_ENV,
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
let mut init_system_import = String::new();
|
||||
if cfg!(feature = "systemd") {
|
||||
write!(
|
||||
init_system_import,
|
||||
"systemctl --user import-environment {variables};"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if cfg!(feature = "dinit") {
|
||||
write!(init_system_import, "dinitctl setenv {variables};").unwrap();
|
||||
}
|
||||
|
||||
fn import_env_to_systemd() {
|
||||
let rv = Command::new("/bin/sh")
|
||||
.args([
|
||||
"-c",
|
||||
"systemctl --user import-environment WAYLAND_DISPLAY && \
|
||||
hash dbus-update-activation-environment 2>/dev/null && \
|
||||
dbus-update-activation-environment WAYLAND_DISPLAY",
|
||||
&format!(
|
||||
"{init_system_import}\
|
||||
hash dbus-update-activation-environment 2>/dev/null && \
|
||||
dbus-update-activation-environment {variables}"
|
||||
),
|
||||
])
|
||||
.spawn();
|
||||
// Wait for the import process to complete, otherwise services will start too fast without
|
||||
@@ -284,7 +301,7 @@ fn import_env_to_systemd() {
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("error spawning shell to import environment into systemd: {err:?}");
|
||||
warn!("error spawning shell to import environment: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,3 +316,15 @@ fn default_config_path() -> Option<PathBuf> {
|
||||
path.push("config.kdl");
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn notify_fd() -> anyhow::Result<()> {
|
||||
let fd = match env::var("NOTIFY_FD") {
|
||||
Ok(notify_fd) => notify_fd.parse()?,
|
||||
Err(env::VarError::NotPresent) => return Ok(()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
env::remove_var("NOTIFY_FD");
|
||||
let mut notif = unsafe { File::from_raw_fd(fd) };
|
||||
notif.write_all(b"READY=1\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+469
-501
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,466 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr;
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
pub struct ForeignToplevelManagerState {
|
||||
display: DisplayHandle,
|
||||
instances: Vec<ZwlrForeignToplevelManagerV1>,
|
||||
toplevels: HashMap<WlSurface, ToplevelData>,
|
||||
}
|
||||
|
||||
pub trait ForeignToplevelHandler {
|
||||
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
|
||||
fn activate(&mut self, wl_surface: WlSurface);
|
||||
fn close(&mut self, wl_surface: WlSurface);
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
|
||||
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
|
||||
}
|
||||
|
||||
struct ToplevelData {
|
||||
title: Option<String>,
|
||||
app_id: Option<String>,
|
||||
states: ArrayVec<u32, 3>,
|
||||
output: Option<Output>,
|
||||
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
// FIXME: parent.
|
||||
}
|
||||
|
||||
pub struct ForeignToplevelGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ForeignToplevelManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = ForeignToplevelGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
|
||||
Self {
|
||||
display: display.clone(),
|
||||
instances: Vec::new(),
|
||||
toplevels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(state: &mut State) {
|
||||
let _span = tracy_client::span!("foreign_toplevel::refresh");
|
||||
|
||||
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||
|
||||
// Handle closed windows.
|
||||
protocol_state.toplevels.retain(|surface, data| {
|
||||
if state.niri.layout.find_window_and_output(surface).is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for instance in data.instances.keys() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
// Handle new and existing windows.
|
||||
//
|
||||
// Save the focused window for last, this way when the focus changes, we will first deactivate
|
||||
// the previous window and only then activate the newly focused window.
|
||||
let mut focused = None;
|
||||
state.niri.layout.with_windows(|window, output| {
|
||||
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();
|
||||
|
||||
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
|
||||
focused = Some((window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
|
||||
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
|
||||
|
||||
let Some(client) = wl_output.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||
for data in protocol_state.toplevels.values_mut() {
|
||||
if data.output.as_ref() != Some(output) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if instance.client().as_ref() != Some(&client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
instance.output_enter(wl_output);
|
||||
instance.done();
|
||||
outputs.push(wl_output.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_toplevel(
|
||||
protocol_state: &mut ForeignToplevelManagerState,
|
||||
wl_surface: &WlSurface,
|
||||
role: &XdgToplevelSurfaceRoleAttributes,
|
||||
output: Option<&Output>,
|
||||
has_focus: bool,
|
||||
) {
|
||||
let states = to_state_vec(&role.current.states, has_focus);
|
||||
|
||||
match protocol_state.toplevels.entry(wl_surface.clone()) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Existing window, check if anything changed.
|
||||
let data = entry.into_mut();
|
||||
|
||||
let mut new_title = None;
|
||||
if data.title != role.title {
|
||||
data.title = role.title.clone();
|
||||
new_title = role.title.as_deref();
|
||||
|
||||
if new_title.is_none() {
|
||||
error!("toplevel title changed to None");
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_app_id = None;
|
||||
if data.app_id != role.app_id {
|
||||
data.app_id = role.app_id.clone();
|
||||
new_app_id = role.app_id.as_deref();
|
||||
|
||||
if new_app_id.is_none() {
|
||||
error!("toplevel app_id changed to None");
|
||||
}
|
||||
}
|
||||
|
||||
let mut states_changed = false;
|
||||
if data.states != states {
|
||||
data.states = states;
|
||||
states_changed = true;
|
||||
}
|
||||
|
||||
let mut output_changed = false;
|
||||
if data.output.as_ref() != output {
|
||||
data.output = output.cloned();
|
||||
output_changed = true;
|
||||
}
|
||||
|
||||
let something_changed =
|
||||
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
|
||||
|
||||
if something_changed {
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
if let Some(new_app_id) = new_app_id {
|
||||
instance.app_id(new_app_id.to_owned());
|
||||
}
|
||||
if states_changed {
|
||||
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||
}
|
||||
if output_changed {
|
||||
for wl_output in outputs.drain(..) {
|
||||
instance.output_leave(&wl_output);
|
||||
}
|
||||
if let Some(output) = &data.output {
|
||||
if let Some(client) = instance.client() {
|
||||
for wl_output in output.client_outputs(&client) {
|
||||
instance.output_enter(&wl_output);
|
||||
outputs.push(wl_output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instance.done();
|
||||
}
|
||||
}
|
||||
|
||||
for outputs in data.instances.values_mut() {
|
||||
// Clean up dead wl_outputs.
|
||||
outputs.retain(|x| x.is_alive());
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
// New window, start tracking it.
|
||||
let mut data = ToplevelData {
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
states,
|
||||
output: output.cloned(),
|
||||
instances: HashMap::new(),
|
||||
};
|
||||
|
||||
for manager in &protocol_state.instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
entry.insert(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelData {
|
||||
fn add_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: &ZwlrForeignToplevelManagerV1,
|
||||
) where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let toplevel = client
|
||||
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
|
||||
.unwrap();
|
||||
manager.toplevel(&toplevel);
|
||||
|
||||
if let Some(title) = &self.title {
|
||||
toplevel.title(title.clone());
|
||||
}
|
||||
if let Some(app_id) = &self.app_id {
|
||||
toplevel.app_id(app_id.clone());
|
||||
}
|
||||
|
||||
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||
|
||||
let mut outputs = Vec::new();
|
||||
if let Some(output) = &self.output {
|
||||
for wl_output in output.client_outputs(client) {
|
||||
toplevel.output_enter(&wl_output);
|
||||
outputs.push(wl_output);
|
||||
}
|
||||
}
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.instances.insert(toplevel, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
|
||||
for ForeignToplevelManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
resource: New<ZwlrForeignToplevelManagerV1>,
|
||||
_global_data: &ForeignToplevelGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(resource, ());
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.instances.push(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ZwlrForeignToplevelHandleV1,
|
||||
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let protocol_state = state.foreign_toplevel_manager_state();
|
||||
|
||||
let Some((surface, _)) = protocol_state
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, data)| data.instances.contains_key(resource))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let surface = surface.clone();
|
||||
|
||||
match request {
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
|
||||
state.activate(surface);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Close => {
|
||||
state.close(surface);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
|
||||
state.set_fullscreen(surface, output);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
|
||||
state.unset_fullscreen(surface);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ZwlrForeignToplevelHandleV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.instances.retain(|instance, _| instance != resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
|
||||
let mut rv = ArrayVec::new();
|
||||
if states.contains(xdg_toplevel::State::Maximized) {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
|
||||
}
|
||||
if states.contains(xdg_toplevel::State::Fullscreen) {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
|
||||
}
|
||||
|
||||
// HACK: wlr-foreign-toplevel-management states:
|
||||
//
|
||||
// These have the same meaning as the states with the same names defined in xdg-toplevel
|
||||
//
|
||||
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
|
||||
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
|
||||
// handles multiple activated windows correctly uses it in its design in such a way that
|
||||
// keyboard focus would make more sense. Let's do what the clients expect.
|
||||
if has_focus {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod foreign_toplevel;
|
||||
pub mod screencopy;
|
||||
@@ -0,0 +1,386 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
Flags, ZwlrScreencopyFrameV1,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
use smithay::wayland::shm;
|
||||
|
||||
// We do not support copy_with_damage() semantics yet.
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
pub struct ScreencopyManagerState;
|
||||
|
||||
pub struct ScreencopyManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ScreencopyManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||
D: ScreencopyHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = ScreencopyManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
|
||||
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData, D>
|
||||
for ScreencopyManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||
D: ScreencopyHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
_state: &mut D,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
manager: New<ZwlrScreencopyManagerV1>,
|
||||
_manager_state: &ScreencopyManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
data_init.init(manager, ());
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrScreencopyManagerV1, (), D> for ScreencopyManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||
D: ScreencopyHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_manager: &ZwlrScreencopyManagerV1,
|
||||
request: zwlr_screencopy_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let (frame, overlay_cursor, buffer_size, region_loc, output) = match request {
|
||||
zwlr_screencopy_manager_v1::Request::CaptureOutput {
|
||||
frame,
|
||||
overlay_cursor,
|
||||
output,
|
||||
} => {
|
||||
let output = Output::from_resource(&output).unwrap();
|
||||
let buffer_size = output.current_mode().unwrap().size;
|
||||
let region_loc = Point::from((0, 0));
|
||||
|
||||
(frame, overlay_cursor, buffer_size, region_loc, output)
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||
frame,
|
||||
overlay_cursor,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
output,
|
||||
} => {
|
||||
if width <= 0 || height <= 0 {
|
||||
trace!("screencopy client requested invalid sized region");
|
||||
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
|
||||
frame.failed();
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Output::from_resource(&output).unwrap();
|
||||
let output_transform = output.current_transform();
|
||||
let output_physical_size =
|
||||
output_transform.transform_size(output.current_mode().unwrap().size);
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
|
||||
|
||||
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
|
||||
|
||||
let output_scale = output.current_scale().integer_scale();
|
||||
let physical_rect = rect.to_physical(output_scale);
|
||||
|
||||
// Clamp captured region to the output.
|
||||
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
|
||||
trace!("screencopy client requested region outside of output");
|
||||
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
|
||||
frame.failed();
|
||||
return;
|
||||
};
|
||||
|
||||
let untransformed_rect = output_transform
|
||||
.invert()
|
||||
.transform_rect_in(clamped_rect, &output_physical_size);
|
||||
|
||||
(
|
||||
frame,
|
||||
overlay_cursor,
|
||||
untransformed_rect.size,
|
||||
clamped_rect.loc,
|
||||
output,
|
||||
)
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::Destroy => return,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Create the frame.
|
||||
let overlay_cursor = overlay_cursor != 0;
|
||||
let info = ScreencopyFrameInfo {
|
||||
output,
|
||||
overlay_cursor,
|
||||
buffer_size,
|
||||
region_loc,
|
||||
};
|
||||
let frame = data_init.init(
|
||||
frame,
|
||||
ScreencopyFrameState::Pending {
|
||||
info,
|
||||
copied: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
);
|
||||
|
||||
// Send desired SHM buffer parameters.
|
||||
frame.buffer(
|
||||
wl_shm::Format::Argb8888,
|
||||
buffer_size.w as u32,
|
||||
buffer_size.h as u32,
|
||||
buffer_size.w as u32 * 4,
|
||||
);
|
||||
|
||||
// if manager.version() >= 3 {
|
||||
// // Send desired DMA buffer parameters.
|
||||
// frame.linux_dmabuf(
|
||||
// Fourcc::Argb8888 as u32,
|
||||
// buffer_size.w as u32,
|
||||
// buffer_size.h as u32,
|
||||
// );
|
||||
//
|
||||
// // Notify client that all supported buffers were enumerated.
|
||||
// frame.buffer_done();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for wlr-screencopy.
|
||||
pub trait ScreencopyHandler {
|
||||
/// Handle new screencopy request.
|
||||
fn frame(&mut self, frame: Screencopy);
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[macro_export]
|
||||
macro_rules! delegate_screencopy {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: $crate::protocols::screencopy::ScreencopyManagerGlobalData
|
||||
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: ()
|
||||
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::protocols::screencopy::ScreencopyFrameState
|
||||
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreencopyFrameInfo {
|
||||
output: Output,
|
||||
buffer_size: Size<i32, Physical>,
|
||||
region_loc: Point<i32, Physical>,
|
||||
overlay_cursor: bool,
|
||||
}
|
||||
|
||||
pub enum ScreencopyFrameState {
|
||||
Failed,
|
||||
Pending {
|
||||
info: ScreencopyFrameInfo,
|
||||
copied: Arc<AtomicBool>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState, D> for ScreencopyManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||
D: ScreencopyHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
frame: &ZwlrScreencopyFrameV1,
|
||||
request: zwlr_screencopy_frame_v1::Request,
|
||||
data: &ScreencopyFrameState,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
if matches!(request, zwlr_screencopy_frame_v1::Request::Destroy) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (info, copied) = match data {
|
||||
ScreencopyFrameState::Failed => return,
|
||||
ScreencopyFrameState::Pending { info, copied } => (info, copied),
|
||||
};
|
||||
|
||||
if copied.load(Ordering::SeqCst) {
|
||||
frame.post_error(
|
||||
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
|
||||
"copy was already requested",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (buffer, with_damage) = match request {
|
||||
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
|
||||
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
|
||||
buffer_data.format == wl_shm::Format::Argb8888
|
||||
&& buffer_data.stride == info.buffer_size.w * 4
|
||||
&& buffer_data.height == info.buffer_size.h
|
||||
&& shm_len as i32 == buffer_data.stride * buffer_data.height
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
frame.post_error(
|
||||
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
|
||||
"invalid buffer",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
copied.store(true, Ordering::SeqCst);
|
||||
|
||||
state.frame(Screencopy {
|
||||
with_damage,
|
||||
buffer,
|
||||
frame: frame.clone(),
|
||||
info: info.clone(),
|
||||
submitted: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Screencopy frame.
|
||||
pub struct Screencopy {
|
||||
info: ScreencopyFrameInfo,
|
||||
frame: ZwlrScreencopyFrameV1,
|
||||
#[allow(unused)]
|
||||
with_damage: bool,
|
||||
buffer: WlBuffer,
|
||||
submitted: bool,
|
||||
}
|
||||
|
||||
impl Drop for Screencopy {
|
||||
fn drop(&mut self) {
|
||||
if !self.submitted {
|
||||
self.frame.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Screencopy {
|
||||
/// Get the target buffer to copy to.
|
||||
pub fn buffer(&self) -> &WlBuffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn region_loc(&self) -> Point<i32, Physical> {
|
||||
self.info.region_loc
|
||||
}
|
||||
|
||||
pub fn buffer_size(&self) -> Size<i32, Physical> {
|
||||
self.info.buffer_size
|
||||
}
|
||||
|
||||
pub fn output(&self) -> &Output {
|
||||
&self.info.output
|
||||
}
|
||||
|
||||
pub fn overlay_cursor(&self) -> bool {
|
||||
self.info.overlay_cursor
|
||||
}
|
||||
|
||||
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
|
||||
// assert!(self.with_damage);
|
||||
//
|
||||
// for Rectangle { loc, size } in damage {
|
||||
// self.frame
|
||||
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Submit the copied content.
|
||||
pub fn submit(mut self, y_invert: bool) {
|
||||
// Notify client that buffer is ordinary.
|
||||
self.frame.flags(if y_invert {
|
||||
Flags::YInvert
|
||||
} else {
|
||||
Flags::empty()
|
||||
});
|
||||
|
||||
// Notify client about successful copy.
|
||||
let time = UNIX_EPOCH.elapsed().unwrap();
|
||||
let tv_sec_hi = (time.as_secs() >> 32) as u32;
|
||||
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
|
||||
let tv_nsec = time.subsec_nanos();
|
||||
self.frame.ready(tv_sec_hi, tv_sec_lo, tv_nsec);
|
||||
|
||||
// Mark frame as submitted to ensure destructor isn't run.
|
||||
self.submitted = true;
|
||||
}
|
||||
|
||||
// pub fn submit_after_sync<T>(
|
||||
// self,
|
||||
// y_invert: bool,
|
||||
// sync_point: Option<OwnedFd>,
|
||||
// event_loop: &LoopHandle<'_, T>,
|
||||
// ) {
|
||||
// match sync_point {
|
||||
// None => self.submit(y_invert),
|
||||
// Some(sync_fd) => {
|
||||
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
|
||||
// let mut screencopy = Some(self);
|
||||
// event_loop
|
||||
// .insert_source(source, move |_, _, _| {
|
||||
// screencopy.take().unwrap().submit(y_invert);
|
||||
// Ok(PostAction::Remove)
|
||||
// })
|
||||
// .unwrap();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
+25
-19
@@ -7,42 +7,48 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use pipewire::spa::data::DataType;
|
||||
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use pipewire::context::Context;
|
||||
use pipewire::core::Core;
|
||||
use pipewire::main_loop::MainLoop;
|
||||
use pipewire::properties::Properties;
|
||||
use pipewire::spa::buffer::DataType;
|
||||
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use pipewire::spa::param::format_utils::parse_format;
|
||||
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
||||
use pipewire::spa::param::ParamType;
|
||||
use pipewire::spa::pod::serialize::PodSerializer;
|
||||
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
||||
use pipewire::spa::sys::*;
|
||||
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
|
||||
use pipewire::spa::Direction;
|
||||
use pipewire::spa::utils::{
|
||||
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
|
||||
};
|
||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
||||
use pipewire::{Context, Core, MainLoop, Properties};
|
||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use zbus::SignalContext;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct PipeWire {
|
||||
_context: Context<MainLoop>,
|
||||
_context: Context,
|
||||
pub core: Core,
|
||||
}
|
||||
|
||||
pub struct Cast {
|
||||
pub session_id: usize,
|
||||
pub stream: Rc<Stream>,
|
||||
pub stream: Stream,
|
||||
_listener: StreamListener<()>,
|
||||
pub is_active: Rc<Cell<bool>>,
|
||||
pub output: Output,
|
||||
pub size: Size<i32, Physical>,
|
||||
pub cursor_mode: CursorMode,
|
||||
pub last_frame_time: Duration,
|
||||
pub min_time_between_frames: Rc<Cell<Duration>>,
|
||||
@@ -51,7 +57,7 @@ pub struct Cast {
|
||||
|
||||
impl PipeWire {
|
||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||
let main_loop = MainLoop::new().context("error creating MainLoop")?;
|
||||
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
||||
let context = Context::new(&main_loop).context("error creating Context")?;
|
||||
let core = context.connect(None).context("error creating Core")?;
|
||||
|
||||
@@ -66,14 +72,14 @@ impl PipeWire {
|
||||
struct AsFdWrapper(MainLoop);
|
||||
impl AsFd for AsFdWrapper {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
self.0.fd()
|
||||
self.0.loop_().fd()
|
||||
}
|
||||
}
|
||||
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(generic, move |_, wrapper, _| {
|
||||
let _span = tracy_client::span!("pipewire iteration");
|
||||
wrapper.0.iterate(Duration::ZERO);
|
||||
wrapper.0.loop_().iterate(Duration::ZERO);
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -112,13 +118,14 @@ impl PipeWire {
|
||||
|
||||
let mode = output.current_mode().unwrap();
|
||||
let size = mode.size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
let refresh = mode.refresh;
|
||||
|
||||
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
|
||||
.context("error creating Stream")?;
|
||||
|
||||
// Like in good old wayland-rs times...
|
||||
let stream = Rc::new(stream);
|
||||
let node_id = Rc::new(Cell::new(None));
|
||||
let is_active = Rc::new(Cell::new(false));
|
||||
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
||||
@@ -127,10 +134,9 @@ impl PipeWire {
|
||||
let listener = stream
|
||||
.add_local_listener_with_user_data(())
|
||||
.state_changed({
|
||||
let stream = stream.clone();
|
||||
let is_active = is_active.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |old, new| {
|
||||
move |stream, (), old, new| {
|
||||
debug!("pw stream: state changed: {old:?} -> {new:?}");
|
||||
|
||||
match new {
|
||||
@@ -174,7 +180,7 @@ impl PipeWire {
|
||||
})
|
||||
.param_changed({
|
||||
let min_time_between_frames = min_time_between_frames.clone();
|
||||
move |stream, id, _data, pod| {
|
||||
move |stream, (), id, pod| {
|
||||
let id = ParamType::from_raw(id);
|
||||
trace!(?id, "pw stream: param_changed");
|
||||
|
||||
@@ -256,8 +262,7 @@ impl PipeWire {
|
||||
let mut b1 = vec![];
|
||||
// let mut b2 = vec![];
|
||||
let mut params = [
|
||||
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
|
||||
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
|
||||
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
|
||||
];
|
||||
stream.update_params(&mut params).unwrap();
|
||||
}
|
||||
@@ -265,7 +270,7 @@ impl PipeWire {
|
||||
.add_buffer({
|
||||
let dmabufs = dmabufs.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |buffer| {
|
||||
move |_stream, (), buffer| {
|
||||
trace!("pw stream: add_buffer");
|
||||
|
||||
unsafe {
|
||||
@@ -309,7 +314,7 @@ impl PipeWire {
|
||||
})
|
||||
.remove_buffer({
|
||||
let dmabufs = dmabufs.clone();
|
||||
move |buffer| {
|
||||
move |_stream, (), buffer| {
|
||||
trace!("pw stream: remove_buffer");
|
||||
|
||||
unsafe {
|
||||
@@ -383,6 +388,7 @@ impl PipeWire {
|
||||
_listener: listener,
|
||||
is_active,
|
||||
output,
|
||||
size,
|
||||
cursor_mode,
|
||||
last_frame_time: Duration::ZERO,
|
||||
min_time_between_frames,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use glam::Vec2;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
|
||||
use super::renderer::NiriRenderer;
|
||||
use super::shaders::Shaders;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
|
||||
#[derive(Debug)]
|
||||
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
|
||||
|
||||
impl GradientRenderElement {
|
||||
pub fn new(
|
||||
renderer: &mut impl NiriRenderer,
|
||||
scale: Scale<f64>,
|
||||
area: Rectangle<i32, Logical>,
|
||||
gradient_area: Rectangle<i32, Logical>,
|
||||
color_from: [f32; 4],
|
||||
color_to: [f32; 4],
|
||||
angle: f32,
|
||||
) -> Option<Self> {
|
||||
let shader = Shaders::get(renderer).gradient_border.clone()?;
|
||||
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
|
||||
|
||||
let grad_dir = Vec2::from_angle(angle);
|
||||
|
||||
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
|
||||
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
|
||||
|
||||
let mut grad_area_diag = Vec2::new(w, h);
|
||||
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
|
||||
grad_area_diag.x = -w;
|
||||
}
|
||||
|
||||
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
|
||||
if grad_dir.y <= 0. {
|
||||
grad_vec = -grad_vec;
|
||||
}
|
||||
|
||||
let elem = PixelShaderElement::new(
|
||||
shader,
|
||||
area,
|
||||
None,
|
||||
1.,
|
||||
vec![
|
||||
Uniform::new("color_from", color_from),
|
||||
Uniform::new("color_to", color_to),
|
||||
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
|
||||
Uniform::new("grad_width", w),
|
||||
Uniform::new("grad_vec", grad_vec.to_array()),
|
||||
],
|
||||
Kind::Unspecified,
|
||||
);
|
||||
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for GradientRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
self.0.id()
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.0.current_commit()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.0.geometry(scale)
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
self.0.transform()
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.0.src()
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.damage_since(scale, commit)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.opaque_regions(scale)
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
self.0.alpha()
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
self.0.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for GradientRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
self.0.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||
self.0.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
|
||||
use smithay::wayland::shm;
|
||||
|
||||
pub mod gradient;
|
||||
pub mod offscreen;
|
||||
pub mod primary_gpu_pixel_shader;
|
||||
pub mod primary_gpu_texture;
|
||||
pub mod render_elements;
|
||||
pub mod renderer;
|
||||
pub mod shaders;
|
||||
|
||||
pub fn render_to_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
|
||||
let texture: GlesTexture = renderer
|
||||
.create_buffer(fourcc, buffer_size)
|
||||
.context("error creating texture")?;
|
||||
|
||||
renderer
|
||||
.bind(texture.clone())
|
||||
.context("error binding texture")?;
|
||||
|
||||
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
|
||||
Ok((texture, sync_point))
|
||||
}
|
||||
|
||||
pub fn render_and_download(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<GlesMapping> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let (_, sync_point) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||
sync_point.wait();
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
|
||||
.context("error copying framebuffer")?;
|
||||
Ok(mapping)
|
||||
}
|
||||
|
||||
pub fn render_to_vec(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let mapping = render_and_download(renderer, size, scale, transform, fourcc, elements)
|
||||
.context("error rendering")?;
|
||||
let copy = renderer
|
||||
.map_texture(&mapping)
|
||||
.context("error mapping texture")?;
|
||||
Ok(copy.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub fn render_to_dmabuf(
|
||||
renderer: &mut GlesRenderer,
|
||||
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<SyncPoint> {
|
||||
let _span = tracy_client::span!();
|
||||
renderer.bind(dmabuf).context("error binding texture")?;
|
||||
render_elements(renderer, size, scale, transform, elements)
|
||||
}
|
||||
|
||||
pub fn render_to_shm(
|
||||
renderer: &mut GlesRenderer,
|
||||
buffer: &WlBuffer,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
|
||||
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
|
||||
let mapping =
|
||||
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
|
||||
let bytes = renderer
|
||||
.map_texture(&mapping)
|
||||
.context("error mapping texture")?;
|
||||
|
||||
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
|
||||
ensure!(
|
||||
// The buffer prefers pixels in little endian ...
|
||||
buffer_data.format == wl_shm::Format::Argb8888
|
||||
&& buffer_data.stride == size.w * 4
|
||||
&& buffer_data.height == size.h
|
||||
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
|
||||
"invalid buffer format or size"
|
||||
);
|
||||
|
||||
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
|
||||
|
||||
unsafe {
|
||||
let _span = tracy_client::span!("copy_nonoverlapping");
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), shm_buffer.cast(), shm_len);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.context("expected shm buffer, but didn't get one")?
|
||||
}
|
||||
|
||||
fn render_elements(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<SyncPoint> {
|
||||
let transform = transform.invert();
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
|
||||
|
||||
let mut frame = renderer
|
||||
.render(size, transform)
|
||||
.context("error starting frame")?;
|
||||
|
||||
frame
|
||||
.clear([0., 0., 0., 0.], &[output_rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
|
||||
if let Some(mut damage) = output_rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
frame.finish().context("error finishing frame")
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use super::render_to_texture;
|
||||
use super::renderer::AsGlesFrame;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Renders elements into an off-screen buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct OffscreenRenderElement {
|
||||
// The texture, if rendering succeeded.
|
||||
texture: Option<PrimaryGpuTextureRenderElement>,
|
||||
// The fallback buffer in case the rendering fails.
|
||||
fallback: SolidColorRenderElement,
|
||||
}
|
||||
|
||||
impl OffscreenRenderElement {
|
||||
pub fn new(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: i32,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
result_alpha: f32,
|
||||
) -> Self {
|
||||
let _span = tracy_client::span!("OffscreenRenderElement::new");
|
||||
|
||||
let geo = elements
|
||||
.iter()
|
||||
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
|
||||
.reduce(|a, b| a.merge(b))
|
||||
.unwrap_or_default();
|
||||
let logical_size = geo.size.to_logical(scale);
|
||||
|
||||
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
|
||||
let fallback = SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
geo.loc,
|
||||
Scale::from(scale as f64),
|
||||
result_alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elements = elements.iter().rev().map(|ele| {
|
||||
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||
});
|
||||
|
||||
match render_to_texture(
|
||||
renderer,
|
||||
geo.size,
|
||||
Scale::from(scale as f64),
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
) {
|
||||
Ok((texture, _sync_point)) => {
|
||||
let buffer =
|
||||
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
|
||||
let element = TextureRenderElement::from_texture_buffer(
|
||||
geo.loc.to_f64(),
|
||||
&buffer,
|
||||
Some(result_alpha),
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
Self {
|
||||
texture: Some(PrimaryGpuTextureRenderElement(element)),
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error off-screening elements: {err:?}");
|
||||
Self {
|
||||
texture: None,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for OffscreenRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.id()
|
||||
} else {
|
||||
self.fallback.id()
|
||||
}
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.current_commit()
|
||||
} else {
|
||||
self.fallback.current_commit()
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.geometry(scale)
|
||||
} else {
|
||||
self.fallback.geometry(scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.transform()
|
||||
} else {
|
||||
self.fallback.transform()
|
||||
}
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.src()
|
||||
} else {
|
||||
self.fallback.src()
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.damage_since(scale, commit)
|
||||
} else {
|
||||
self.fallback.damage_since(scale, commit)
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.opaque_regions(scale)
|
||||
} else {
|
||||
self.fallback.opaque_regions(scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.alpha()
|
||||
} else {
|
||||
self.fallback.alpha()
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.kind()
|
||||
} else {
|
||||
self.fallback.kind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::renderer::AsGlesFrame;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Wrapper for a poxel shader from the primary GPU for rendering with the primary GPU.
|
||||
#[derive(Debug)]
|
||||
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
|
||||
|
||||
impl Element for PrimaryGpuPixelShaderRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
self.0.id()
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.0.current_commit()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.0.geometry(scale)
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
self.0.transform()
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.0.src()
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.damage_since(scale, commit)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.opaque_regions(scale)
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
self.0.alpha()
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
self.0.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
_renderer: &mut TtyRenderer<'render>,
|
||||
) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,12 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||
};
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::renderer::AsGlesFrame;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Trait with our main renderer requirements to save on the typing.
|
||||
pub trait NiriRenderer:
|
||||
ImportAll
|
||||
+ ImportMem
|
||||
+ ExportMem
|
||||
+ Bind<Dmabuf>
|
||||
+ Offscreen<GlesTexture>
|
||||
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||
+ AsGlesRenderer
|
||||
{
|
||||
// Associated types to work around the instability of associated type bounds.
|
||||
type NiriTextureId: Texture + Clone + 'static;
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<<GlesRenderer as Renderer>::Error>
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
type NiriError = R::Error;
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesRenderer`.
|
||||
pub trait AsGlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||
}
|
||||
|
||||
impl AsGlesRenderer for GlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesFrame`.
|
||||
pub trait AsGlesFrame<'frame>
|
||||
where
|
||||
Self: 'frame,
|
||||
{
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
||||
#[derive(Debug)]
|
||||
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
||||
@@ -142,16 +73,14 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
||||
for PrimaryGpuTextureRenderElement
|
||||
{
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||
Ok(())
|
||||
@@ -159,7 +88,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
||||
_renderer: &mut TtyRenderer<'render>,
|
||||
) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
@@ -0,0 +1,148 @@
|
||||
// We need to implement RenderElement manually due to AsGlesFrame requirement.
|
||||
// This macro does it for us.
|
||||
#[macro_export]
|
||||
macro_rules! niri_render_elements {
|
||||
// The two callable variants: with <R> and without <R>. They include From impls because nested
|
||||
// repetitions ($type and $variant with + and $R with ?) don't work properly.
|
||||
($name:ident<R> => { $($variant:ident = $type:ty),+ $(,)? }) => {
|
||||
$crate::niri_render_elements!(@impl $name () ($name<R>) => { $($variant = $type),+ });
|
||||
|
||||
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
|
||||
fn from(x: $type) -> Self {
|
||||
Self::$variant(x)
|
||||
}
|
||||
})+
|
||||
};
|
||||
|
||||
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
|
||||
$crate::niri_render_elements!(@impl $name ($name) () => { $($variant = $type),+ });
|
||||
|
||||
$(impl From<$type> for $name {
|
||||
fn from(x: $type) -> Self {
|
||||
Self::$variant(x)
|
||||
}
|
||||
})+
|
||||
};
|
||||
|
||||
// The internal variant that generates most of the code. $name_no_R and $name_R are necessary
|
||||
// for the impl RenderElement<SomeRenderer> for $name<SomeRenderer>: since $R does not appear
|
||||
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
|
||||
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
|
||||
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
|
||||
#[derive(Debug)]
|
||||
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
|
||||
$($variant($type)),+
|
||||
}
|
||||
|
||||
impl$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? smithay::backend::renderer::element::Element for $name$(<$R>)? {
|
||||
fn id(&self) -> &smithay::backend::renderer::element::Id {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.id()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.current_commit()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> smithay::utils::Rectangle<i32, smithay::utils::Physical> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.geometry(scale)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> smithay::utils::Transform {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.transform()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.src()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: smithay::utils::Scale<f64>,
|
||||
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
|
||||
) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.opaque_regions(scale)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.alpha()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> smithay::backend::renderer::element::Kind {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.kind()),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
|
||||
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
|
||||
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
renderer: &mut $crate::backend::tty::TtyRenderer<'render>,
|
||||
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||
};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer};
|
||||
|
||||
/// Trait with our main renderer requirements to save on the typing.
|
||||
pub trait NiriRenderer:
|
||||
ImportAll
|
||||
+ ImportMem
|
||||
+ ExportMem
|
||||
+ Bind<Dmabuf>
|
||||
+ Offscreen<GlesTexture>
|
||||
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||
+ AsGlesRenderer
|
||||
{
|
||||
// Associated types to work around the instability of associated type bounds.
|
||||
type NiriTextureId: Texture + Clone + 'static;
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<<GlesRenderer as Renderer>::Error>
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
type NiriError = R::Error;
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesRenderer`.
|
||||
pub trait AsGlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||
}
|
||||
|
||||
impl AsGlesRenderer for GlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesFrame`.
|
||||
pub trait AsGlesFrame<'frame>
|
||||
where
|
||||
Self: 'frame,
|
||||
{
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
precision mediump float;
|
||||
uniform float alpha;
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float tint;
|
||||
#endif
|
||||
uniform vec2 size;
|
||||
varying vec2 v_coords;
|
||||
|
||||
uniform vec4 color_from;
|
||||
uniform vec4 color_to;
|
||||
uniform vec2 grad_offset;
|
||||
uniform float grad_width;
|
||||
uniform vec2 grad_vec;
|
||||
|
||||
void main() {
|
||||
vec2 coords = v_coords * size + grad_offset;
|
||||
|
||||
if ((grad_vec.x < 0.0 && 0.0 <= grad_vec.y) || (0.0 <= grad_vec.x && grad_vec.y < 0.0))
|
||||
coords.x -= grad_width;
|
||||
|
||||
float frac = dot(coords, grad_vec) / dot(grad_vec, grad_vec);
|
||||
|
||||
if (grad_vec.y < 0.0)
|
||||
frac += 1.0;
|
||||
|
||||
frac = clamp(frac, 0.0, 1.0);
|
||||
vec4 out_color = mix(color_from, color_to, frac);
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
if (tint == 1.0)
|
||||
out_color = vec4(0.0, 0.3, 0.0, 0.2) + out_color * 0.8;
|
||||
#endif
|
||||
|
||||
gl_FragColor = out_color;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
|
||||
pub struct Shaders {
|
||||
pub gradient_border: Option<GlesPixelProgram>,
|
||||
}
|
||||
|
||||
impl Shaders {
|
||||
fn compile(renderer: &mut GlesRenderer) -> Self {
|
||||
let _span = tracy_client::span!("Shaders::compile");
|
||||
|
||||
let gradient_border = renderer
|
||||
.compile_custom_pixel_shader(
|
||||
include_str!("gradient_border.frag"),
|
||||
&[
|
||||
UniformName::new("color_from", UniformType::_4f),
|
||||
UniformName::new("color_to", UniformType::_4f),
|
||||
UniformName::new("grad_offset", UniformType::_2f),
|
||||
UniformName::new("grad_width", UniformType::_1f),
|
||||
UniformName::new("grad_vec", UniformType::_2f),
|
||||
],
|
||||
)
|
||||
.map_err(|err| {
|
||||
warn!("error compiling gradient border shader: {err:?}");
|
||||
})
|
||||
.ok();
|
||||
|
||||
Self { gradient_border }
|
||||
}
|
||||
|
||||
pub fn get(renderer: &mut impl NiriRenderer) -> &Self {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let data = renderer.egl_context().user_data();
|
||||
data.get()
|
||||
.expect("shaders::init() must be called when creating the renderer")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(renderer: &mut GlesRenderer) {
|
||||
let shaders = Shaders::compile(renderer);
|
||||
let data = renderer.egl_context().user_data();
|
||||
if !data.insert_if_missing(|| shaders) {
|
||||
error!("shaders were already compiled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RubberBand {
|
||||
pub stiffness: f64,
|
||||
pub limit: f64,
|
||||
}
|
||||
|
||||
impl RubberBand {
|
||||
pub fn band(&self, x: f64) -> f64 {
|
||||
let c = self.stiffness;
|
||||
let d = self.limit;
|
||||
|
||||
(1. - (1. / (x * c / d + 1.))) * d
|
||||
}
|
||||
|
||||
pub fn derivative(&self, x: f64) -> f64 {
|
||||
let c = self.stiffness;
|
||||
let d = self.limit;
|
||||
|
||||
c * d * d / (c * x + d).powi(2)
|
||||
}
|
||||
|
||||
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
|
||||
let clamped = x.clamp(min, max);
|
||||
let sign = if x < clamped { -1. } else { 1. };
|
||||
let diff = (x - clamped).abs();
|
||||
|
||||
clamped + sign * self.band(diff)
|
||||
}
|
||||
|
||||
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
|
||||
if min <= x && x <= max {
|
||||
return 1.;
|
||||
}
|
||||
|
||||
let clamped = x.clamp(min, max);
|
||||
let diff = (x - clamped).abs();
|
||||
self.derivative(diff)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
|
||||
const DECELERATION_TOUCHPAD: f64 = 0.997;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SwipeTracker {
|
||||
history: VecDeque<Event>,
|
||||
pos: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Event {
|
||||
delta: f64,
|
||||
timestamp: Duration,
|
||||
}
|
||||
|
||||
impl SwipeTracker {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
history: VecDeque::new(),
|
||||
pos: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a new reading into the tracker.
|
||||
pub fn push(&mut self, delta: f64, timestamp: Duration) {
|
||||
// For the events that we care about, timestamps should always increase
|
||||
// monotonically.
|
||||
if let Some(last) = self.history.back() {
|
||||
if timestamp < last.timestamp {
|
||||
trace!(
|
||||
"ignoring event with timestamp {timestamp:?} earlier than last {:?}",
|
||||
last.timestamp
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.history.push_back(Event { delta, timestamp });
|
||||
self.pos += delta;
|
||||
|
||||
self.trim_history();
|
||||
}
|
||||
|
||||
/// Returns the current gesture position.
|
||||
pub fn pos(&self) -> f64 {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Computes the current gesture velocity.
|
||||
pub fn velocity(&self) -> f64 {
|
||||
let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else {
|
||||
return 0.;
|
||||
};
|
||||
|
||||
let total_time = (last.timestamp - first.timestamp).as_secs_f64();
|
||||
if total_time == 0. {
|
||||
return 0.;
|
||||
}
|
||||
|
||||
let total_delta = self.history.iter().map(|event| event.delta).sum::<f64>();
|
||||
total_delta / total_time
|
||||
}
|
||||
|
||||
/// Computes the gesture end position after decelerating to a halt.
|
||||
pub fn projected_end_pos(&self) -> f64 {
|
||||
let vel = self.velocity();
|
||||
self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln())
|
||||
}
|
||||
|
||||
fn trim_history(&mut self) {
|
||||
let Some(&Event { timestamp, .. }) = self.history.back() else {
|
||||
return;
|
||||
};
|
||||
|
||||
while let Some(first) = self.history.front() {
|
||||
if timestamp <= first.timestamp + HISTORY_LIMIT {
|
||||
break;
|
||||
}
|
||||
|
||||
let _ = self.history.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::Config;
|
||||
use pangocairo::cairo::{self, ImageSurface};
|
||||
use pangocairo::pango::FontDescription;
|
||||
use smithay::backend::renderer::element::memory::{
|
||||
@@ -14,7 +17,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const TEXT: &str = "Failed to parse the config file. \
|
||||
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
||||
@@ -26,6 +29,12 @@ const BORDER: i32 = 4;
|
||||
pub struct ConfigErrorNotification {
|
||||
state: State,
|
||||
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
||||
|
||||
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
|
||||
// notification.
|
||||
created_path: Option<PathBuf>,
|
||||
|
||||
config: Rc<RefCell<Config>>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
@@ -39,16 +48,43 @@ pub type ConfigErrorNotificationRenderElement<R> =
|
||||
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||
|
||||
impl ConfigErrorNotification {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(config: Rc<RefCell<Config>>) -> Self {
|
||||
Self {
|
||||
state: State::Hidden,
|
||||
buffers: RefCell::new(HashMap::new()),
|
||||
created_path: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn animation(&self, from: f64, to: f64) -> Animation {
|
||||
let c = self.config.borrow();
|
||||
Animation::new(
|
||||
from,
|
||||
to,
|
||||
0.,
|
||||
c.animations.config_notification_open_close,
|
||||
niri_config::Animation::default_config_notification_open_close(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
|
||||
if self.created_path != created_path {
|
||||
self.created_path = created_path;
|
||||
self.buffers.borrow_mut().clear();
|
||||
}
|
||||
|
||||
self.state = State::Showing(self.animation(0., 1.));
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
if self.created_path.is_some() {
|
||||
self.created_path = None;
|
||||
self.buffers.borrow_mut().clear();
|
||||
}
|
||||
|
||||
// Show from scratch even if already showing to bring attention.
|
||||
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
|
||||
self.state = State::Showing(self.animation(0., 1.));
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
@@ -56,7 +92,7 @@ impl ConfigErrorNotification {
|
||||
return;
|
||||
}
|
||||
|
||||
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
|
||||
self.state = State::Hiding(self.animation(1., 0.));
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
|
||||
@@ -65,7 +101,15 @@ impl ConfigErrorNotification {
|
||||
State::Showing(anim) => {
|
||||
anim.set_current_time(target_presentation_time);
|
||||
if anim.is_done() {
|
||||
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
|
||||
let duration = if self.created_path.is_some() {
|
||||
// Make this quite a bit longer because it comes with a monitor modeset
|
||||
// (can take a while) and an important hotkeys popup diverting the
|
||||
// attention.
|
||||
Duration::from_secs(8)
|
||||
} else {
|
||||
Duration::from_secs(4)
|
||||
};
|
||||
self.state = State::Shown(target_presentation_time + duration);
|
||||
}
|
||||
}
|
||||
State::Shown(deadline) => {
|
||||
@@ -75,7 +119,9 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
State::Hiding(anim) => {
|
||||
anim.set_current_time(target_presentation_time);
|
||||
if anim.is_done() {
|
||||
// HACK: prevent bounciness on hiding. This is better done with a clamp property on
|
||||
// the spring animation.
|
||||
if anim.is_done() || anim.value() <= 0. {
|
||||
self.state = State::Hidden;
|
||||
}
|
||||
}
|
||||
@@ -96,11 +142,12 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let path = self.created_path.as_deref();
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let buffer = buffers
|
||||
.entry(scale)
|
||||
.or_insert_with_key(move |&scale| render(scale).ok());
|
||||
.or_insert_with_key(move |&scale| render(scale, path).ok());
|
||||
let buffer = buffer.as_ref()?;
|
||||
|
||||
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||
@@ -138,19 +185,30 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
let _span = tracy_client::span!("config_error_notification::render");
|
||||
|
||||
let padding = PADDING * scale;
|
||||
|
||||
let mut text = String::from(TEXT);
|
||||
let mut border_color = (1., 0.3, 0.3);
|
||||
if let Some(path) = created_path {
|
||||
text = format!(
|
||||
"Created a default config file at \
|
||||
<span face='monospace' bgcolor='#000000'>{:?}</span>",
|
||||
path
|
||||
);
|
||||
border_color = (0.5, 1., 0.5);
|
||||
};
|
||||
|
||||
let mut font = FontDescription::from_string(FONT);
|
||||
font.set_absolute_size((font.size() * scale).into());
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&text);
|
||||
|
||||
let (mut width, mut height) = layout.pixel_size();
|
||||
width += padding * 2;
|
||||
@@ -166,25 +224,25 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&text);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(0., 0.);
|
||||
cr.line_to(width.into(), 0.);
|
||||
cr.line_to(width.into(), height.into());
|
||||
cr.line_to(0., height.into());
|
||||
cr.line_to(0., 0.);
|
||||
cr.set_source_rgb(1., 0.3, 0.3);
|
||||
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
|
||||
cr.set_line_width((BORDER * scale).into());
|
||||
cr.stroke()?;
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
@@ -12,7 +12,7 @@ use smithay::output::Output;
|
||||
use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
||||
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
||||
@@ -111,7 +111,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
@@ -130,13 +130,13 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(0., 0.);
|
||||
cr.line_to(width.into(), 0.);
|
||||
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
@@ -18,7 +18,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::{Physical, Size, Transform};
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const PADDING: i32 = 8;
|
||||
const MARGIN: i32 = PADDING * 2;
|
||||
@@ -155,13 +155,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
let binds = &config.binds.0;
|
||||
|
||||
// Collect actions that we want to show.
|
||||
let mut actions = vec![
|
||||
&Action::ShowHotkeyOverlay,
|
||||
&Action::Quit,
|
||||
&Action::CloseWindow,
|
||||
];
|
||||
let mut actions = vec![&Action::ShowHotkeyOverlay];
|
||||
|
||||
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
|
||||
// Quit(false).
|
||||
if binds.iter().any(|bind| bind.action == Action::Quit(false)) {
|
||||
actions.push(&Action::Quit(false));
|
||||
} else if binds.iter().any(|bind| bind.action == Action::Quit(true)) {
|
||||
actions.push(&Action::Quit(true));
|
||||
} else {
|
||||
actions.push(&Action::Quit(false));
|
||||
}
|
||||
|
||||
actions.extend(&[
|
||||
&Action::CloseWindow,
|
||||
&Action::FocusColumnLeft,
|
||||
&Action::FocusColumnRight,
|
||||
&Action::MoveColumnLeft,
|
||||
@@ -173,12 +180,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
|
||||
if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown))
|
||||
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
|
||||
{
|
||||
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||
} else if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
|
||||
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
|
||||
{
|
||||
actions.push(&Action::MoveWindowToWorkspaceDown);
|
||||
} else {
|
||||
@@ -188,12 +195,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
// Same for -up.
|
||||
if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
|
||||
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
|
||||
{
|
||||
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||
} else if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
|
||||
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
|
||||
{
|
||||
actions.push(&Action::MoveWindowToWorkspaceUp);
|
||||
} else {
|
||||
@@ -208,20 +215,26 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
]);
|
||||
|
||||
// Screenshot is not as important, can omit if not bound.
|
||||
if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
|
||||
{
|
||||
if binds.iter().any(|bind| bind.action == Action::Screenshot) {
|
||||
actions.push(&Action::Screenshot);
|
||||
}
|
||||
|
||||
// Add the spawn actions.
|
||||
for bind in binds
|
||||
.iter()
|
||||
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
|
||||
{
|
||||
actions.push(bind.actions.first().unwrap());
|
||||
let mut spawn_actions = Vec::new();
|
||||
for bind in binds.iter().filter(|bind| {
|
||||
matches!(bind.action, Action::Spawn(_))
|
||||
// Only show binds with Mod or Super to filter out stuff like volume up/down.
|
||||
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|
||||
|| bind.key.modifiers.contains(Modifiers::SUPER))
|
||||
}) {
|
||||
let action = &bind.action;
|
||||
|
||||
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
|
||||
if !spawn_actions.contains(&action) {
|
||||
spawn_actions.push(action);
|
||||
}
|
||||
}
|
||||
actions.extend(spawn_actions);
|
||||
|
||||
let strings = actions
|
||||
.into_iter()
|
||||
@@ -230,7 +243,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
.binds
|
||||
.0
|
||||
.iter()
|
||||
.find(|bind| bind.actions.first() == Some(action))
|
||||
.find(|bind| bind.action == *action)
|
||||
.map(|bind| key_name(comp_mod, &bind.key))
|
||||
.unwrap_or_else(|| String::from("(not bound)"));
|
||||
|
||||
@@ -243,7 +256,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
let bold = AttrList::new();
|
||||
@@ -298,7 +311,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
@@ -306,20 +319,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
|
||||
layout.set_attributes(Some(&bold));
|
||||
layout.set_text(TITLE);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
|
||||
|
||||
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
|
||||
layout.set_attributes(Some(&attrs));
|
||||
layout.set_text(key);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.rel_move_to((key_width + padding).into(), 0.);
|
||||
|
||||
layout.set_attributes(None);
|
||||
layout.set_markup(action);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.rel_move_to(
|
||||
(-(key_width + padding)).into(),
|
||||
@@ -338,7 +351,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
@@ -356,7 +369,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
|
||||
fn action_name(action: &Action) -> String {
|
||||
match action {
|
||||
Action::Quit => String::from("Exit niri"),
|
||||
Action::Quit(_) => String::from("Exit niri"),
|
||||
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
|
||||
Action::CloseWindow => String::from("Close Focused Window"),
|
||||
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod config_error_notification;
|
||||
pub mod exit_confirm_dialog;
|
||||
pub mod hotkey_overlay;
|
||||
pub mod screenshot_ui;
|
||||
@@ -10,16 +10,15 @@ use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::{ButtonState, MouseButton};
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::ExportMem;
|
||||
use smithay::input::keyboard::{Keysym, ModifiersState};
|
||||
use smithay::output::{Output, WeakOutput};
|
||||
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
|
||||
const BORDER: i32 = 2;
|
||||
|
||||
@@ -42,16 +41,18 @@ pub enum ScreenshotUi {
|
||||
pub struct OutputData {
|
||||
size: Size<i32, Physical>,
|
||||
scale: i32,
|
||||
transform: Transform,
|
||||
texture: GlesTexture,
|
||||
texture_buffer: TextureBuffer<GlesTexture>,
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
locations: [Point<i32, Physical>; 8],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScreenshotUiRenderElement {
|
||||
Screenshot(PrimaryGpuTextureRenderElement),
|
||||
SolidColor(SolidColorRenderElement),
|
||||
niri_render_elements! {
|
||||
ScreenshotUiRenderElement => {
|
||||
Screenshot = PrimaryGpuTextureRenderElement,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenshotUi {
|
||||
@@ -94,6 +95,7 @@ impl ScreenshotUi {
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let scale = selection.0.current_scale().integer_scale();
|
||||
let selection = (
|
||||
selection.0,
|
||||
@@ -104,9 +106,9 @@ impl ScreenshotUi {
|
||||
let output_data = screenshots
|
||||
.into_iter()
|
||||
.map(|(output, texture)| {
|
||||
let output_transform = output.current_transform();
|
||||
let transform = output.current_transform();
|
||||
let output_mode = output.current_mode().unwrap();
|
||||
let size = output_transform.transform_size(output_mode.size);
|
||||
let size = transform.transform_size(output_mode.size);
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let texture_buffer = TextureBuffer::from_texture(
|
||||
renderer,
|
||||
@@ -129,6 +131,7 @@ impl ScreenshotUi {
|
||||
let data = OutputData {
|
||||
size,
|
||||
scale,
|
||||
transform,
|
||||
texture,
|
||||
texture_buffer,
|
||||
buffers,
|
||||
@@ -333,10 +336,10 @@ impl ScreenshotUi {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
|
||||
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
|
||||
if let Self::Open { output_data, .. } = self {
|
||||
let data = output_data.get(output)?;
|
||||
Some((data.size, data.scale))
|
||||
Some((data.size, data.scale, data.transform))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -448,138 +451,3 @@ pub fn rect_from_corner_points(
|
||||
let y2 = max(a.y, b.y);
|
||||
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
|
||||
}
|
||||
|
||||
// Manual RenderElement implementation due to AsGlesFrame requirement.
|
||||
impl Element for ScreenshotUiRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.id(),
|
||||
Self::SolidColor(elem) => elem.id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.current_commit(),
|
||||
Self::SolidColor(elem) => elem.current_commit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.geometry(scale),
|
||||
Self::SolidColor(elem) => elem.geometry(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.transform(),
|
||||
Self::SolidColor(elem) => elem.transform(),
|
||||
}
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.src(),
|
||||
Self::SolidColor(elem) => elem.src(),
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.damage_since(scale, commit),
|
||||
Self::SolidColor(elem) => elem.damage_since(scale, commit),
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.opaque_regions(scale),
|
||||
Self::SolidColor(elem) => elem.opaque_regions(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.alpha(),
|
||||
Self::SolidColor(elem) => elem.alpha(),
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Screenshot(elem) => elem.kind(),
|
||||
Self::SolidColor(elem) => elem.kind(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for ScreenshotUiRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => {
|
||||
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
||||
}
|
||||
Self::SolidColor(elem) => {
|
||||
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for ScreenshotUiRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'render, 'alloc, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
||||
match self {
|
||||
Self::Screenshot(elem) => {
|
||||
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
||||
}
|
||||
Self::SolidColor(elem) => {
|
||||
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
||||
) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SolidColorRenderElement> for ScreenshotUiRenderElement {
|
||||
fn from(x: SolidColorRenderElement) -> Self {
|
||||
Self::SolidColor(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrimaryGpuTextureRenderElement> for ScreenshotUiRenderElement {
|
||||
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
|
||||
Self::Screenshot(x)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,36 @@
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::io::{self, Write};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ptr::null_mut;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use directories::UserDirs;
|
||||
use git_version::git_version;
|
||||
use niri_config::Config;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
pub mod spawning;
|
||||
pub mod watcher;
|
||||
|
||||
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
||||
(t.0.clone(), t.1.clone())
|
||||
}
|
||||
|
||||
pub fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
git_version!(fallback = "unknown commit"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_monotonic_time() -> Duration {
|
||||
let ts = clock_gettime(ClockId::Monotonic);
|
||||
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
|
||||
@@ -39,6 +50,15 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
|
||||
.to_logical(output_scale)
|
||||
}
|
||||
|
||||
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
|
||||
if let Ok(rest) = path.strip_prefix("~") {
|
||||
let dirs = UserDirs::new().context("error retrieving home directory")?;
|
||||
Ok(Some([dirs.home_dir(), rest].iter().collect()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
||||
let Some(path) = &config.screenshot_path else {
|
||||
return Ok(None);
|
||||
@@ -61,91 +81,13 @@ pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>>
|
||||
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
|
||||
}
|
||||
|
||||
if let Ok(rest) = path.strip_prefix("~") {
|
||||
let dirs = UserDirs::new().context("error retrieving home directory")?;
|
||||
path = [dirs.home_dir(), rest].iter().collect();
|
||||
if let Some(expanded) = expand_home(&path).context("error expanding ~")? {
|
||||
path = expanded;
|
||||
}
|
||||
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Spawns the command to run independently of the compositor.
|
||||
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
if command.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawning and waiting takes some milliseconds, so do it in a thread.
|
||||
let res = thread::Builder::new()
|
||||
.name("Command Spawner".to_owned())
|
||||
.spawn(move || {
|
||||
let (command, args) = command.split_first().unwrap();
|
||||
spawn_sync(command, args);
|
||||
});
|
||||
|
||||
if let Err(err) = res {
|
||||
warn!("error spawning a thread to spawn the command: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let command = command.as_ref();
|
||||
|
||||
let mut process = Command::new(command);
|
||||
process
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
|
||||
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
|
||||
process.env_remove("RUST_BACKTRACE");
|
||||
}
|
||||
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
|
||||
process.env_remove("RUST_LIB_BACKTRACE");
|
||||
}
|
||||
|
||||
// Double-fork to avoid having to waitpid the child.
|
||||
unsafe {
|
||||
process.pre_exec(|| {
|
||||
match libc::fork() {
|
||||
-1 => return Err(io::Error::last_os_error()),
|
||||
0 => (),
|
||||
_ => libc::_exit(0),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let mut child = match process.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
warn!("error spawning {command:?}: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match child.wait() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
warn!("child did not exit successfully: {status:?}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error waiting for child: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_png_rgba8(
|
||||
w: impl Write,
|
||||
width: u32,
|
||||
@@ -0,0 +1,324 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use std::{io, thread};
|
||||
|
||||
use libc::close_range;
|
||||
use niri_config::Environment;
|
||||
use smithay::reexports::rustix;
|
||||
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
|
||||
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
|
||||
|
||||
use crate::utils::expand_home;
|
||||
|
||||
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
|
||||
|
||||
/// Spawns the command to run independently of the compositor.
|
||||
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
if command.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawning and waiting takes some milliseconds, so do it in a thread.
|
||||
let res = thread::Builder::new()
|
||||
.name("Command Spawner".to_owned())
|
||||
.spawn(move || {
|
||||
let (command, args) = command.split_first().unwrap();
|
||||
spawn_sync(command, args);
|
||||
});
|
||||
|
||||
if let Err(err) = res {
|
||||
warn!("error spawning a thread to spawn the command: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let mut command = command.as_ref();
|
||||
|
||||
// Expand `~` at the start.
|
||||
let expanded = expand_home(Path::new(command));
|
||||
match &expanded {
|
||||
Ok(Some(expanded)) => command = expanded.as_ref(),
|
||||
Ok(None) => (),
|
||||
Err(err) => {
|
||||
warn!("error expanding ~: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut process = Command::new(command);
|
||||
process
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
|
||||
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
|
||||
process.env_remove("RUST_BACKTRACE");
|
||||
}
|
||||
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
|
||||
process.env_remove("RUST_LIB_BACKTRACE");
|
||||
}
|
||||
|
||||
// Set configured environment.
|
||||
let env = CHILD_ENV.read().unwrap();
|
||||
for var in &env.0 {
|
||||
if let Some(value) = &var.value {
|
||||
process.env(&var.name, value);
|
||||
} else {
|
||||
process.env_remove(&var.name);
|
||||
}
|
||||
}
|
||||
drop(env);
|
||||
|
||||
// When running as a systemd session, we want to put children into their own transient scopes
|
||||
// in order to separate them from the niri process. This is helpful for example to prevent the
|
||||
// OOM killer from taking down niri together with a misbehaving client.
|
||||
//
|
||||
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
|
||||
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits at
|
||||
// just the right time, the transient unit will be created but empty, so it will linger around
|
||||
// forever.
|
||||
//
|
||||
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
|
||||
// intermediate child we will send back the grandchild PID, and in niri we will create a
|
||||
// transient scope with both our intermediate child and the grandchild PIDs set. Only then we
|
||||
// will signal our intermediate child to exit. This way, even if the grandchild exits quickly,
|
||||
// a non-empty scope will be created (with just our intermediate child), then cleaned up when
|
||||
// our intermediate child exits.
|
||||
|
||||
// Make a pipe to receive the grandchild PID.
|
||||
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
|
||||
.map_err(|err| {
|
||||
warn!("error creating a pipe to transfer child PID: {err:?}");
|
||||
})
|
||||
.ok()
|
||||
.unzip();
|
||||
// Make a pipe to wait in the intermediate child.
|
||||
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
|
||||
.map_err(|err| {
|
||||
warn!("error creating a pipe for child to wait on: {err:?}");
|
||||
})
|
||||
.ok()
|
||||
.unzip();
|
||||
|
||||
unsafe {
|
||||
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
|
||||
// the raw fd inside so that it's not closed any extra times.
|
||||
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
|
||||
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
|
||||
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
|
||||
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
|
||||
|
||||
// Double-fork to avoid having to waitpid the child.
|
||||
process.pre_exec(move || {
|
||||
// Close FDs that we don't need. Especially important for the write ones to unblock the
|
||||
// readers.
|
||||
if let Some(fd) = pipe_pid_read_fd.take() {
|
||||
close(fd);
|
||||
}
|
||||
if let Some(fd) = pipe_wait_write_fd.take() {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
|
||||
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
|
||||
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
|
||||
|
||||
match libc::fork() {
|
||||
-1 => return Err(io::Error::last_os_error()),
|
||||
0 => (),
|
||||
grandchild_pid => {
|
||||
// Send back the PID.
|
||||
if let Some(pipe) = pipe_pid_write {
|
||||
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
|
||||
}
|
||||
|
||||
// Wait until the parent signals us to exit.
|
||||
if let Some(pipe) = pipe_wait_read {
|
||||
// We're going to exit afterwards. Close all other FDs to allow
|
||||
// Command::spawn() to return in the parent process.
|
||||
let raw = pipe.as_raw_fd() as u32;
|
||||
let _ = close_range(0, raw - 1, 0);
|
||||
let _ = close_range(raw + 1, !0, 0);
|
||||
|
||||
let _ = read_all(pipe, &mut [0]);
|
||||
}
|
||||
|
||||
libc::_exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let mut child = match process.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
warn!("error spawning {command:?}: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
drop(pipe_pid_write);
|
||||
drop(pipe_wait_read);
|
||||
|
||||
// Wait for the grandchild PID.
|
||||
if let Some(pipe) = pipe_pid_read {
|
||||
let mut buf = [0; 4];
|
||||
match read_all(pipe, &mut buf) {
|
||||
Ok(()) => {
|
||||
let pid = i32::from_ne_bytes(buf);
|
||||
trace!("spawned PID: {pid}");
|
||||
|
||||
// Start a systemd scope for the grandchild.
|
||||
#[cfg(feature = "systemd")]
|
||||
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
|
||||
trace!("error starting systemd scope for spawned command: {err:?}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error reading child PID: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signal the intermediate child to exit now that we're done trying to creating a systemd scope.
|
||||
trace!("signaling child to exit");
|
||||
drop(pipe_wait_write);
|
||||
|
||||
match child.wait() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
warn!("child did not exit successfully: {status:?}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error waiting for child: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
|
||||
let mut written = 0;
|
||||
loop {
|
||||
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
|
||||
if n == 0 {
|
||||
return Err(rustix::io::Errno::CANCELED);
|
||||
}
|
||||
|
||||
written += n;
|
||||
if written == buf.len() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
|
||||
let mut start = 0;
|
||||
loop {
|
||||
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
|
||||
if n == 0 {
|
||||
return Err(rustix::io::Errno::CANCELED);
|
||||
}
|
||||
|
||||
start += n;
|
||||
if start == buf.len() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts a (newly spawned) pid into a transient systemd scope.
|
||||
///
|
||||
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
|
||||
/// from bringing down the compositor together with a misbehaving client.
|
||||
#[cfg(feature = "systemd")]
|
||||
fn start_systemd_scope(name: &OsStr, intermediate_pid: u32, child_pid: u32) -> anyhow::Result<()> {
|
||||
use std::fmt::Write as _;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use zbus::zvariant::{OwnedObjectPath, Value};
|
||||
|
||||
use crate::utils::IS_SYSTEMD_SERVICE;
|
||||
|
||||
// We only start transient scopes if we're a systemd service ourselves.
|
||||
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
// Extract the basename.
|
||||
let name = Path::new(name).file_name().unwrap_or(name);
|
||||
|
||||
let mut scope_name = String::from("app-niri-");
|
||||
|
||||
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
|
||||
// systemd source.
|
||||
for &c in name.as_bytes() {
|
||||
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
|
||||
scope_name.push(char::from(c));
|
||||
} else {
|
||||
let _ = write!(scope_name, "\\x{c:02x}");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = write!(scope_name, "-{child_pid}.scope");
|
||||
|
||||
// Ask systemd to start a transient scope.
|
||||
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
|
||||
let conn = CONNECTION
|
||||
.get_or_init(zbus::blocking::Connection::session)
|
||||
.clone()
|
||||
.context("error connecting to session bus")?;
|
||||
|
||||
let proxy = zbus::blocking::Proxy::new(
|
||||
&conn,
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager",
|
||||
)
|
||||
.context("error creating a Proxy")?;
|
||||
|
||||
let signals = proxy
|
||||
.receive_signal("JobRemoved")
|
||||
.context("error creating a signal iterator")?;
|
||||
|
||||
let pids: &[_] = &[intermediate_pid, child_pid];
|
||||
let properties: &[_] = &[
|
||||
("PIDs", Value::new(pids)),
|
||||
("CollectMode", Value::new("inactive-or-failed")),
|
||||
];
|
||||
let aux: &[(&str, &[(&str, Value)])] = &[];
|
||||
|
||||
let job: OwnedObjectPath = proxy
|
||||
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
|
||||
.context("error calling StartTransientUnit")?;
|
||||
|
||||
trace!("waiting for JobRemoved");
|
||||
for message in signals {
|
||||
let body: (u32, OwnedObjectPath, &str, &str) =
|
||||
message.body().context("error parsing signal")?;
|
||||
|
||||
if body.1 == job {
|
||||
// Our transient unit had started, we're good to exit the intermediate child.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
//! File modification watcher.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::reexports::calloop::channel::SyncSender;
|
||||
|
||||
pub struct Watcher {
|
||||
should_stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for Watcher {
|
||||
fn drop(&mut self) {
|
||||
self.should_stop.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
|
||||
Self::with_start_notification(path, changed, None)
|
||||
}
|
||||
|
||||
pub fn with_start_notification(
|
||||
path: PathBuf,
|
||||
changed: SyncSender<()>,
|
||||
started: Option<mpsc::SyncSender<()>>,
|
||||
) -> Self {
|
||||
let should_stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
{
|
||||
let should_stop = should_stop.clone();
|
||||
thread::Builder::new()
|
||||
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
||||
.spawn(move || {
|
||||
// this "should" be as simple as mtime, but it does not quite work in practice;
|
||||
// it doesn't work if the config is a symlink, and its target changes but the
|
||||
// new target and old target have identical mtimes.
|
||||
//
|
||||
// in practice, this does not occur on any systems other than nix.
|
||||
// because, on nix practically everything is a symlink to /nix/store
|
||||
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
|
||||
// so, symlink targets change frequently when mtime doesn't.
|
||||
let mut last_props = path
|
||||
.canonicalize()
|
||||
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||
.ok();
|
||||
|
||||
if let Some(started) = started {
|
||||
let _ = started.send(());
|
||||
}
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
if should_stop.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(new_props) = path
|
||||
.canonicalize()
|
||||
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||
{
|
||||
if last_props.as_ref() != Some(&new_props) {
|
||||
trace!("file changed: {}", path.to_string_lossy());
|
||||
|
||||
if let Err(err) = changed.send(()) {
|
||||
warn!("error sending change notification: {err:?}");
|
||||
break;
|
||||
}
|
||||
|
||||
last_props = Some(new_props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("exiting watcher thread for {}", path.to_string_lossy());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Self { should_stop }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
|
||||
use calloop::channel::sync_channel;
|
||||
use calloop::EventLoop;
|
||||
use smithay::reexports::rustix::fs::{futimens, Timestamps};
|
||||
use smithay::reexports::rustix::time::Timespec;
|
||||
use xshell::{cmd, Shell};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn check(
|
||||
setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
|
||||
change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
|
||||
) {
|
||||
let sh = Shell::new().unwrap();
|
||||
let temp_dir = sh.create_temp_dir().unwrap();
|
||||
sh.change_dir(temp_dir.path());
|
||||
// let dir = sh.create_dir("xshell").unwrap();
|
||||
// sh.change_dir(dir);
|
||||
|
||||
let mut config_path = sh.current_dir();
|
||||
config_path.push("niri");
|
||||
config_path.push("config.kdl");
|
||||
|
||||
setup(&sh).unwrap();
|
||||
|
||||
let changed = AtomicU8::new(0);
|
||||
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
let (tx, rx) = sync_channel(1);
|
||||
let (started_tx, started_rx) = mpsc::sync_channel(1);
|
||||
let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
|
||||
loop_handle
|
||||
.insert_source(rx, |_, _, _| {
|
||||
changed.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
.unwrap();
|
||||
started_rx.recv().unwrap();
|
||||
|
||||
// HACK: if we don't sleep, files might have the same mtime.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
change(&sh).unwrap();
|
||||
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(750), &mut ())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(changed.load(Ordering::SeqCst), 1);
|
||||
|
||||
// Verify that the watcher didn't break.
|
||||
sh.write_file(&config_path, "c").unwrap();
|
||||
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(750), &mut ())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(changed.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_file() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "b")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_file() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.create_dir("niri")?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dir_and_file() {
|
||||
check(
|
||||
|_sh| Ok(()),
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_linked_file() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri/config2.kdl", "a")?;
|
||||
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri/config2.kdl", "b")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_file_in_linked_dir() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri2/config.kdl", "a")?;
|
||||
cmd!(sh, "ln -s niri2 niri").run()?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri2/config.kdl", "b")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recreate_file() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.remove_path("niri/config.kdl")?;
|
||||
sh.write_file("niri/config.kdl", "b")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recreate_dir() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.remove_path("niri")?;
|
||||
sh.write_file("niri/config.kdl", "b")?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_dir() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri/config.kdl", "a")?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri2/config.kdl", "b")?;
|
||||
sh.remove_path("niri")?;
|
||||
cmd!(sh, "mv niri2 niri").run()?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_just_link() {
|
||||
// NixOS setup: link path changes, mtime stays constant.
|
||||
check(
|
||||
|sh| {
|
||||
let mut dir = sh.current_dir();
|
||||
dir.push("niri");
|
||||
sh.create_dir(&dir)?;
|
||||
|
||||
let mut d2 = dir.clone();
|
||||
d2.push("config2.kdl");
|
||||
let mut c2 = File::create(d2).unwrap();
|
||||
write!(c2, "a")?;
|
||||
c2.flush()?;
|
||||
futimens(
|
||||
&c2,
|
||||
&Timestamps {
|
||||
last_access: Timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
},
|
||||
last_modification: Timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
c2.sync_all()?;
|
||||
drop(c2);
|
||||
|
||||
let mut d3 = dir.clone();
|
||||
d3.push("config3.kdl");
|
||||
let mut c3 = File::create(d3).unwrap();
|
||||
write!(c3, "b")?;
|
||||
c3.flush()?;
|
||||
futimens(
|
||||
&c3,
|
||||
&Timestamps {
|
||||
last_access: Timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
},
|
||||
last_modification: Timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
c3.sync_all()?;
|
||||
drop(c3);
|
||||
|
||||
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
cmd!(sh, "unlink niri/config.kdl").run()?;
|
||||
cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_dir_link() {
|
||||
check(
|
||||
|sh| {
|
||||
sh.write_file("niri2/config.kdl", "a")?;
|
||||
cmd!(sh, "ln -s niri2 niri").run()?;
|
||||
Ok(())
|
||||
},
|
||||
|sh| {
|
||||
sh.write_file("niri3/config.kdl", "b")?;
|
||||
cmd!(sh, "unlink niri").run()?;
|
||||
cmd!(sh, "ln -s niri3 niri").run()?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
//! File modification watcher.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::reexports::calloop::channel::SyncSender;
|
||||
|
||||
pub struct Watcher {
|
||||
should_stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for Watcher {
|
||||
fn drop(&mut self) {
|
||||
self.should_stop.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
|
||||
let should_stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
{
|
||||
let should_stop = should_stop.clone();
|
||||
thread::Builder::new()
|
||||
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
||||
.spawn(move || {
|
||||
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
if should_stop.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
|
||||
if last_mtime != Some(mtime) {
|
||||
trace!("file changed: {}", path.to_string_lossy());
|
||||
|
||||
if let Err(err) = changed.send(()) {
|
||||
warn!("error sending change notification: {err:?}");
|
||||
break;
|
||||
}
|
||||
|
||||
last_mtime = Some(mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("exiting watcher thread for {}", path.to_string_lossy());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Self { should_stop }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unmapped {
|
||||
pub window: Window,
|
||||
pub state: InitialConfigureState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InitialConfigureState {
|
||||
/// The window has not been initially configured yet.
|
||||
NotConfigured {
|
||||
/// Whether the window requested to be fullscreened, and the requested output, if any.
|
||||
wants_fullscreen: Option<Option<Output>>,
|
||||
},
|
||||
/// The window has been configured.
|
||||
Configured {
|
||||
/// Up-to-date rules.
|
||||
///
|
||||
/// We start tracking window rules when sending the initial configure, since they don't
|
||||
/// affect anything before that.
|
||||
rules: ResolvedWindowRules,
|
||||
|
||||
/// Resolved default width for this window.
|
||||
///
|
||||
/// `None` means that the window will pick its own width.
|
||||
width: Option<ColumnWidth>,
|
||||
|
||||
/// Whether the window should open full-width.
|
||||
is_full_width: bool,
|
||||
|
||||
/// Output to open this window on.
|
||||
///
|
||||
/// This can be `None` in cases like:
|
||||
///
|
||||
/// - There are no outputs connected.
|
||||
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
|
||||
/// should fetch the parent's current output again upon mapping.
|
||||
output: Option<Output>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Rules fully resolved for a window.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResolvedWindowRules {
|
||||
/// Default width for this window.
|
||||
///
|
||||
/// - `None`: unset (global default should be used).
|
||||
/// - `Some(None)`: set to empty (window picks its own width).
|
||||
/// - `Some(Some(width))`: set to a particular width.
|
||||
pub default_width: Option<Option<ColumnWidth>>,
|
||||
|
||||
/// Output to open this window on.
|
||||
pub open_on_output: Option<String>,
|
||||
|
||||
/// Whether the window should open full-width.
|
||||
pub open_maximized: Option<bool>,
|
||||
|
||||
/// Whether the window should open fullscreen.
|
||||
pub open_fullscreen: Option<bool>,
|
||||
}
|
||||
|
||||
impl Unmapped {
|
||||
/// Wraps a newly created window that hasn't been initially configured yet.
|
||||
pub fn new(window: Window) -> Self {
|
||||
Self {
|
||||
window,
|
||||
state: InitialConfigureState::NotConfigured {
|
||||
wants_fullscreen: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_initial_configure(&self) -> bool {
|
||||
matches!(self.state, InitialConfigureState::NotConfigured { .. })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user