mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4adf56b49 | |||
| 0a54db6b93 | |||
| 5f7d06713e | |||
| f0de0d77d6 |
@@ -1 +0,0 @@
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
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. -->
|
||||
|
||||
<!-- Attaching your full niri config can help diagnose the problem. -->
|
||||
|
||||
<!--
|
||||
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
|
||||
|
||||
You can also check what process the window PID belongs to:
|
||||
|
||||
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
|
||||
|
||||
If this points to xwayland-satellite, then it's an X11 window.
|
||||
|
||||
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
|
||||
-->
|
||||
|
||||
### System Information
|
||||
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
||||
* Distro:
|
||||
|
||||
<!-- 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:
|
||||
@@ -1,10 +0,0 @@
|
||||
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)
|
||||
- name: Ask a question
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
|
||||
about: Question about niri (start a Discussion)
|
||||
- name: Matrix room
|
||||
url: https://matrix.to/#/#niri:matrix.org
|
||||
about: Chat about niri with other users
|
||||
@@ -1,22 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
smithay:
|
||||
patterns:
|
||||
- "smithay"
|
||||
- "smithay-drm-extras"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
+29
-199
@@ -9,8 +9,6 @@ on:
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -25,7 +23,7 @@ jobs:
|
||||
release-flag: '--release'
|
||||
|
||||
name: test - ${{ matrix.configuration }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -34,130 +32,41 @@ 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 ${{ env.DEPS_APT }}
|
||||
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
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.configuration }}
|
||||
|
||||
- name: Check (no default features)
|
||||
run: cargo check ${{ matrix.release-flag }} --no-default-features
|
||||
- name: Build (no default features)
|
||||
run: cargo build ${{ matrix.release-flag }} --no-default-features
|
||||
|
||||
- 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
|
||||
run: cargo build ${{ 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 --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||
- name: Build Tests
|
||||
run: cargo test --no-run --all ${{ matrix.release-flag }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
# Job that runs randomized tests for a longer period of time.
|
||||
randomized-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: randomized tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
PROPTEST_CASES: 200000
|
||||
PROPTEST_MAX_LOCAL_REJECTS: 200000
|
||||
PROPTEST_MAX_GLOBAL_REJECTS: 200000
|
||||
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests --release
|
||||
|
||||
visual-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} 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
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- run: cargo check --all-targets
|
||||
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: clippy
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -166,12 +75,15 @@ 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 ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
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
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal --component clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -179,101 +91,19 @@ jobs:
|
||||
run: cargo clippy --all --all-targets
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install nightly --profile minimal --component rustfmt
|
||||
rustup override set nightly
|
||||
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo dnf update -y
|
||||
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build --all
|
||||
|
||||
nix:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v4
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
check-links:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
|
||||
with:
|
||||
args: --offline --include-fragments 'wiki/*.md'
|
||||
|
||||
publish-wiki:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- build
|
||||
- check-links
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
|
||||
|
||||
rustdoc:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Generate documentation
|
||||
run: cargo doc --no-deps -p niri-ipc
|
||||
|
||||
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
|
||||
|
||||
- name: Deploy documentation
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc
|
||||
force_orphan: true
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Prepare release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Public version'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Create vendored dependencies archive
|
||||
run: |
|
||||
mkdir .cargo
|
||||
cargo vendor --locked > .cargo/config.toml
|
||||
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --frozen --release
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --frozen --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --frozen --release -- --nocapture
|
||||
|
||||
- name: Draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
|
||||
fail_on_unmatched_files: true
|
||||
@@ -1,2 +1 @@
|
||||
/target
|
||||
/result
|
||||
|
||||
Generated
+1262
-2376
File diff suppressed because it is too large
Load Diff
+42
-125
@@ -1,100 +1,48 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"niri-config",
|
||||
"niri-ipc",
|
||||
"niri-visual-tests",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.2.0"
|
||||
[package]
|
||||
name = "niri"
|
||||
version = "0.1.0-alpha.2"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.80.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.97"
|
||||
bitflags = "2.9.0"
|
||||
clap = { version = "4.5.34", features = ["derive"] }
|
||||
insta = "1.42.2"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.0", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
# version = "0.1.0"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
[package]
|
||||
name = "niri"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.3.1"
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.22.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.47"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
anyhow = { version = "1.0.79" }
|
||||
arrayvec = "0.7.4"
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
bitflags = "2.4.1"
|
||||
clap = { version = "4.4.13", features = ["derive"] }
|
||||
directories = "5.0.1"
|
||||
git-version = "0.3.9"
|
||||
glam = "0.30.1"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.171"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.2.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.0.0"
|
||||
pango = { version = "0.20.9", features = ["v1_44"] }
|
||||
pangocairo = "0.20.7"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.5"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.4", optional = true }
|
||||
wayland-backend = "0.3.8"
|
||||
wayland-scanner = "0.31.6"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "5.5.0", optional = true }
|
||||
libc = "0.2.151"
|
||||
logind-zbus = { version = "3.1.2", optional = true }
|
||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
notify-rust = { version = "4.10.0", optional = true }
|
||||
pipewire = { version = "0.7.2", optional = true }
|
||||
png = "0.17.10"
|
||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.13"
|
||||
sd-notify = "0.4.1"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_with = "3.4.0"
|
||||
toml = { version = "0.8.8", default-features = false, features = ["parse"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracy-client = { version = "0.16.5", default-features = false }
|
||||
url = { version = "2.5.0", optional = true }
|
||||
xcursor = "0.3.5"
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
features = [
|
||||
"backend_drm",
|
||||
"backend_egl",
|
||||
@@ -105,54 +53,35 @@ features = [
|
||||
"backend_winit",
|
||||
"desktop",
|
||||
"renderer_gl",
|
||||
"renderer_pixman",
|
||||
"renderer_multi",
|
||||
"use_system_lib",
|
||||
"wayland_frontend",
|
||||
]
|
||||
|
||||
[dependencies.smithay-drm-extras]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.0"
|
||||
insta.workspace = true
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||
rayon = "1.10.0"
|
||||
wayland-client = "0.31.8"
|
||||
xshell = "0.2.7"
|
||||
proptest = "1.4.0"
|
||||
proptest-derive = "0.4.0"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
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"]
|
||||
# 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 the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
profile-with-tracy-allocations = ["profile-with-tracy"]
|
||||
# Enables dinit integration (global environment).
|
||||
dinit = []
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
overflow-checks = true
|
||||
lto = "thin"
|
||||
|
||||
[profile.release.package.niri-config]
|
||||
# knuffel with chomsky generates a metric ton of debuginfo.
|
||||
debug = false
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "25.02"
|
||||
version = "0.1.0~alpha.2"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
@@ -163,15 +92,3 @@ assets = [
|
||||
]
|
||||
[package.metadata.generate-rpm.requires]
|
||||
alacritty = "*"
|
||||
fuzzel = "*"
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "alacritty, fuzzel"
|
||||
assets = [
|
||||
["target/release/niri", "usr/bin/", "755"],
|
||||
["resources/niri-session", "usr/bin/", "755"],
|
||||
["resources/niri.desktop", "/usr/share/wayland-sessions/", "644"],
|
||||
["resources/niri-portals.conf", "/usr/share/xdg-desktop-portal/", "644"],
|
||||
["resources/niri.service", "/usr/lib/systemd/user/", "644"],
|
||||
["resources/niri-shutdown.target", "/usr/lib/systemd/user/", "644"],
|
||||
]
|
||||
|
||||
@@ -1,116 +1,163 @@
|
||||
<h1 align="center">niri</h1>
|
||||
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||
<p align="center">
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||
</p>
|
||||
# niri
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
A scrollable-tiling Wayland compositor.
|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
Windows are arranged in columns on an infinite strip going to the right.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
- Built from the ground up for scrollable tiling
|
||||
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
|
||||
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- Live-reloading config
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||

|
||||
|
||||
## Status
|
||||
|
||||
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
|
||||
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
|
||||
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.
|
||||
|
||||
Give it a try!
|
||||
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
|
||||
Here are some points you may have questions about:
|
||||
## Idea
|
||||
|
||||
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
|
||||
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
|
||||
- **NVIDIA**: seems to work fine.
|
||||
- **Floating windows**: yes, starting from niri 25.01.
|
||||
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
|
||||
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
|
||||
We have touchpad gestures, but no touchscreen gestures yet.
|
||||
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
|
||||
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
|
||||
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
|
||||
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
|
||||
- Steam and games, including Proton: work perfectly through xwayland-satellite.
|
||||
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
|
||||
- Discord and other Electron apps: work well through xwayland-satellite.
|
||||
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
|
||||
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
|
||||
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
|
||||
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||
Niri implements scrollable tiling, heavily inspired by [PaperWM].
|
||||
Windows are arranged in columns on an infinite strip going to the right.
|
||||
Every column takes up a full monitor worth of height, divided among its windows.
|
||||
|
||||
## Inspiration
|
||||
With multiple monitors, every monitor has its own separate window strip.
|
||||
Windows can never "overflow" onto an adjacent monitor.
|
||||
|
||||
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
||||
This is one of the reasons that prompted me to try writing my own compositor.
|
||||
PaperWM is a solid implementation, but, being a GNOME Shell extension, it has to work around Shell's global window coordinate space to prevent windows from overflowing.
|
||||
|
||||
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 also has dynamic workspaces which work similar to GNOME Shell.
|
||||
Since windows go left-to-right horizontally, workspaces are arranged vertically.
|
||||
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
||||
|
||||
## Tile Scrollably Elsewhere
|
||||
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.
|
||||
|
||||
Here are some other projects which implement a similar workflow:
|
||||
## Building
|
||||
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
> [!TIP]
|
||||
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||
>
|
||||
> NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||
|
||||
## Media
|
||||
First, install the dependencies for your distribution.
|
||||
|
||||
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
|
||||
- Ubuntu:
|
||||
|
||||
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
|
||||
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
|
||||
```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
|
||||
```
|
||||
|
||||
## Contact
|
||||
- Fedora:
|
||||
|
||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||
```sh
|
||||
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel clang
|
||||
```
|
||||
|
||||
Next, build niri with `cargo build --release`.
|
||||
|
||||
## Installation
|
||||
|
||||
The recommended way to install and run niri is as a standalone desktop session.
|
||||
To do that, put files into the correct directories according to this table.
|
||||
|
||||
| File | Destination |
|
||||
| ---- | ----------- |
|
||||
| `target/release/niri` | `/usr/bin/` |
|
||||
| `resources/niri-session` | `/usr/bin/` |
|
||||
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
|
||||
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
|
||||
| `resources/niri.service` | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
|
||||
|
||||
Doing this will make niri appear in GDM and, presumably, other display managers.
|
||||
|
||||
## Running
|
||||
|
||||
`cargo run --release`
|
||||
|
||||
Inside an existing desktop session, it will run in a window.
|
||||
On a TTY, it will run natively.
|
||||
|
||||
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
|
||||
|
||||
### Session
|
||||
|
||||
If you followed the recommended installation steps above, niri should appear in your display manager.
|
||||
Starting it from there will run niri as a desktop session.
|
||||
|
||||
The niri session will autostart apps through the systemd xdg-autostart target.
|
||||
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
|
||||
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
|
||||
|
||||
Niri also works with some parts of xdg-desktop-portal-gnome.
|
||||
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
|
||||
|
||||
### Xwayland
|
||||
|
||||
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
|
||||
|
||||
## Default Hotkeys
|
||||
|
||||
When running on a TTY, the Mod key is <kbd>Super</kbd>.
|
||||
When running in a window, the Mod key is <kbd>Alt</kbd>.
|
||||
|
||||
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
|
||||
|
||||
| Hotkey | Description |
|
||||
| ------ | ----------- |
|
||||
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
|
||||
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
|
||||
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
|
||||
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
|
||||
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
|
||||
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
|
||||
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column |
|
||||
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
|
||||
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused window to the monitor to the side |
|
||||
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
|
||||
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused window to the workspace below |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused window to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>1</kbd>–<kbd>9</kbd> | Switch to a workspace by index |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd>–<kbd>9</kbd> | Move the focused window to a workspace by index |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
|
||||
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
|
||||
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
|
||||
|
||||
## Configuration
|
||||
|
||||
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
|
||||
If this fails, it will load [the default configuration file](resources/default-config.kdl).
|
||||
Please use the default configuration file as the starting point for your custom configuration.
|
||||
|
||||
Niri will live-reload many of the configuration settings, like key binds or gaps, as you change the config file.
|
||||
Though, some settings are still missing live-reload support.
|
||||
Notably, output modes and positions will only apply when the output is reconnected.
|
||||
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[gamescope]: https://github.com/ValveSoftware/gamescope
|
||||
[mako]: https://github.com/emersion/mako
|
||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
ignore-interior-mutability = [
|
||||
"smithay::desktop::Window",
|
||||
"smithay::output::Output",
|
||||
"wayland_server::backend::ClientId",
|
||||
]
|
||||
Generated
-64
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1731533336,
|
||||
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1742707865,
|
||||
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1742697269,
|
||||
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
# This flake file is community maintained
|
||||
{
|
||||
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-filter,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
cairo,
|
||||
dbus,
|
||||
libGL,
|
||||
libdisplay-info,
|
||||
libinput,
|
||||
seatd,
|
||||
libxkbcommon,
|
||||
libgbm,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
installShellFiles,
|
||||
withDbus ? true,
|
||||
withSystemd ? true,
|
||||
withScreencastSupport ? true,
|
||||
withDinit ? false,
|
||||
}:
|
||||
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = self;
|
||||
include = [
|
||||
"niri-config"
|
||||
"niri-ipc"
|
||||
"niri-visual-tests"
|
||||
"resources"
|
||||
"src"
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
];
|
||||
};
|
||||
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
# NOTE: This is only used for Git dependencies
|
||||
allowBuiltinFetchGit = true;
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
rustPlatform.bindgenHook
|
||||
pkg-config
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
[
|
||||
cairo
|
||||
dbus
|
||||
libGL
|
||||
libdisplay-info
|
||||
libinput
|
||||
seatd
|
||||
libxkbcommon
|
||||
libgbm
|
||||
pango
|
||||
wayland
|
||||
]
|
||||
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
|
||||
++ lib.optional withScreencastSupport pipewire
|
||||
# Also includes libudev
|
||||
++ lib.optional withSystemd systemd;
|
||||
|
||||
buildFeatures =
|
||||
lib.optional withDbus "dbus"
|
||||
++ lib.optional withDinit "dinit"
|
||||
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
|
||||
++ lib.optional withSystemd "systemd";
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
# ever since this commit:
|
||||
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
|
||||
# and thus creates a real socket file in the runtime dir.
|
||||
# this is fine for our build, we just need to make sure it has a directory to write to.
|
||||
preCheck = ''
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
'';
|
||||
|
||||
postInstall =
|
||||
''
|
||||
installShellCompletion --cmd niri \
|
||||
--bash <($out/bin/niri completions bash) \
|
||||
--fish <($out/bin/niri completions fish) \
|
||||
--zsh <($out/bin/niri completions zsh)
|
||||
|
||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
passthru = {
|
||||
providedSessions = [ "niri" ];
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
inherit (nixpkgs) lib;
|
||||
# Support all Linux systems that the nixpkgs flake exposes
|
||||
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
|
||||
|
||||
forAllSystems = lib.genAttrs systems;
|
||||
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
checks = forAllSystems (system: {
|
||||
# We use the debug build here to save a bit of time
|
||||
inherit (self.packages.${system}) niri-debug;
|
||||
});
|
||||
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
pkgs.pkg-config
|
||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
buildInputs = niri.buildInputs ++ [
|
||||
pkgs.libadwaita # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
env = {
|
||||
# WARN: Do not overwrite this variable in your shell!
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
niri = nixpkgsFor.${system}.callPackage niri-package { };
|
||||
in
|
||||
{
|
||||
inherit niri;
|
||||
|
||||
# NOTE: This is for development purposes only
|
||||
#
|
||||
# It is primarily to help with quickly iterating on
|
||||
# changes made to the above expression - though it is
|
||||
# also not stripped in order to better debug niri itself
|
||||
niri-debug = niri.overrideAttrs (
|
||||
newAttrs: oldAttrs: {
|
||||
pname = oldAttrs.pname + "-debug";
|
||||
|
||||
cargoBuildType = "debug";
|
||||
cargoCheckType = newAttrs.cargoBuildType;
|
||||
|
||||
dontStrip = true;
|
||||
}
|
||||
);
|
||||
|
||||
default = niri;
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: _: {
|
||||
niri = final.callPackage niri-package { };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "niri-config"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
pretty_assertions = "1.4.1"
|
||||
@@ -1,26 +0,0 @@
|
||||
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
#[knuffel(children(name = "match"))]
|
||||
pub matches: Vec<Match>,
|
||||
#[knuffel(children(name = "exclude"))]
|
||||
pub excludes: Vec<Match>,
|
||||
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub opacity: Option<f32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
#[knuffel(child, default)]
|
||||
pub shadow: ShadowRule,
|
||||
#[knuffel(child)]
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct Match {
|
||||
#[knuffel(property, str)]
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
/// `Regex` that implements `PartialEq` by its string form.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegexEq(pub Regex);
|
||||
|
||||
impl PartialEq for RegexEq {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_str() == other.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RegexEq {}
|
||||
|
||||
impl FromStr for RegexEq {
|
||||
type Err = <Regex as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Regex::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct KdlCodeBlock {
|
||||
filename: String,
|
||||
code: String,
|
||||
line_number: usize,
|
||||
must_fail: bool,
|
||||
}
|
||||
|
||||
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
|
||||
let mut lines = file_contents
|
||||
.lines()
|
||||
.map(|line| {
|
||||
// Removes the > from callouts that might contain ```kdl```
|
||||
let line = line.trim();
|
||||
if line.starts_with('>') {
|
||||
if line.len() == 1 {
|
||||
""
|
||||
} else {
|
||||
&line[2..]
|
||||
}
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.enumerate();
|
||||
|
||||
let mut kdl_code_blocks = vec![];
|
||||
|
||||
while let Some((line_number, line)) = lines.next() {
|
||||
if !line.starts_with("```kdl") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut snippet = String::new();
|
||||
|
||||
for (_, line) in lines
|
||||
.by_ref()
|
||||
.take_while(|(_, line)| !line.starts_with("```"))
|
||||
{
|
||||
snippet.push_str(line);
|
||||
snippet.push('\n');
|
||||
}
|
||||
|
||||
kdl_code_blocks.push(KdlCodeBlock {
|
||||
code: snippet,
|
||||
line_number,
|
||||
filename: filename.to_string(),
|
||||
must_fail: line.contains("must-fail"),
|
||||
});
|
||||
}
|
||||
|
||||
kdl_code_blocks
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_docs_parses() {
|
||||
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
|
||||
|
||||
let code_blocks = fs::read_dir(wiki_dir)
|
||||
.unwrap()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
|
||||
.filter(|file| {
|
||||
file.path()
|
||||
.extension()
|
||||
.map(|ext| ext == "md")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.flat_map(|file| {
|
||||
let file_contents = fs::read_to_string(file.path()).unwrap();
|
||||
let file_path = file.path();
|
||||
let filename = file_path.to_str().unwrap();
|
||||
extract_kdl_from_file(&file_contents, filename)
|
||||
});
|
||||
|
||||
let mut errors = vec![];
|
||||
|
||||
for KdlCodeBlock {
|
||||
code,
|
||||
line_number,
|
||||
filename,
|
||||
must_fail,
|
||||
} in code_blocks
|
||||
{
|
||||
if let Err(error) = niri_config::Config::parse(&filename, &code) {
|
||||
if !must_fail {
|
||||
errors.push(format!(
|
||||
"Error parsing wiki KDL code block at {}:{}: {:?}",
|
||||
filename,
|
||||
line_number,
|
||||
miette::Report::new(error)
|
||||
));
|
||||
}
|
||||
} else if must_fail {
|
||||
errors.push(format!(
|
||||
"Expected error parsing wiki KDL code block at {}:{}",
|
||||
filename, line_number
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
panic!(
|
||||
"Errors parsing {} wiki KDL code blocks:\n{}",
|
||||
errors.len(),
|
||||
errors.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "niri-ipc"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
description = "Types and helpers for interfacing with the niri Wayland compositor."
|
||||
keywords = ["wayland"]
|
||||
categories = ["api-bindings", "os"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "0.8.22", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
json-schema = ["dep:schemars"]
|
||||
@@ -1,16 +0,0 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
This crate follows the niri version.
|
||||
It is **not** API-stable in terms of the Rust semver.
|
||||
In particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
|
||||
Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.2.0"
|
||||
```
|
||||
-1426
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
//! Helper for blocking communication over the niri socket.
|
||||
|
||||
use std::env;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Event, Reply, Request};
|
||||
|
||||
/// Name of the environment variable containing the niri IPC socket path.
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
|
||||
/// Helper for blocking communication over the niri socket.
|
||||
///
|
||||
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
||||
/// and serialization/deserialization of messages.
|
||||
pub struct Socket {
|
||||
stream: UnixStream,
|
||||
}
|
||||
|
||||
impl Socket {
|
||||
/// Connects to the default niri IPC socket.
|
||||
///
|
||||
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
|
||||
/// [`SOCKET_PATH_ENV`] environment variable.
|
||||
pub fn connect() -> io::Result<Self> {
|
||||
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
|
||||
)
|
||||
})?;
|
||||
Self::connect_to(socket_path)
|
||||
}
|
||||
|
||||
/// Connects to the niri IPC socket at the given path.
|
||||
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let stream = UnixStream::connect(path.as_ref())?;
|
||||
Ok(Self { stream })
|
||||
}
|
||||
|
||||
/// Sends a request to niri and returns the response.
|
||||
///
|
||||
/// Return values:
|
||||
///
|
||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||
/// * `Ok(Err(message))`: error message from niri
|
||||
/// * `Err(error)`: error communicating with niri
|
||||
///
|
||||
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
|
||||
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
|
||||
/// otherwise.
|
||||
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
|
||||
let Self { mut stream } = self;
|
||||
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
stream.write_all(buf.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
|
||||
let events = move || {
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
let event = serde_json::from_str(&buf)?;
|
||||
Ok(event)
|
||||
};
|
||||
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
//! Helpers for keeping track of the event stream state.
|
||||
//!
|
||||
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
|
||||
//! you only care about part of the state.
|
||||
//! 2. Connect to the niri socket and request an event stream.
|
||||
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
|
||||
//! 4. Read the fields of the state as needed.
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
/// Returns a sequence of events that replicates this state from default initialization.
|
||||
fn replicate(&self) -> Vec<Event>;
|
||||
|
||||
/// Applies the event to this state.
|
||||
///
|
||||
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
|
||||
/// part of the state.
|
||||
fn apply(&mut self, event: Event) -> Option<Event>;
|
||||
}
|
||||
|
||||
/// The full state communicated over the event stream.
|
||||
///
|
||||
/// Different parts of the state are not guaranteed to be consistent across every single event
|
||||
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
|
||||
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
|
||||
/// these two events, the workspace active window id refers to a window that does not yet exist in
|
||||
/// the windows state part.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EventStreamState {
|
||||
/// State of workspaces.
|
||||
pub workspaces: WorkspacesState,
|
||||
|
||||
/// State of workspaces.
|
||||
pub windows: WindowsState,
|
||||
|
||||
/// State of the keyboard layouts.
|
||||
pub keyboard_layouts: KeyboardLayoutsState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WorkspacesState {
|
||||
/// Map from a workspace id to the workspace.
|
||||
pub workspaces: HashMap<u64, Workspace>,
|
||||
}
|
||||
|
||||
/// The windows state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WindowsState {
|
||||
/// Map from a window id to the window.
|
||||
pub windows: HashMap<u64, Window>,
|
||||
}
|
||||
|
||||
/// The keyboard layout state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KeyboardLayoutsState {
|
||||
/// Configured keyboard layouts.
|
||||
pub keyboard_layouts: Option<KeyboardLayouts>,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.workspaces.replicate());
|
||||
events.extend(self.windows.replicate());
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
let event = self.workspaces.apply(event)?;
|
||||
let event = self.windows.apply(event)?;
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WorkspacesState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let workspaces = self.workspaces.values().cloned().collect();
|
||||
vec![Event::WorkspacesChanged { workspaces }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let ws = self.workspaces.get(&id);
|
||||
let ws = ws.expect("activated workspace was missing from the map");
|
||||
let output = ws.output.clone();
|
||||
|
||||
for ws in self.workspaces.values_mut() {
|
||||
let got_activated = ws.id == id;
|
||||
if ws.output == output {
|
||||
ws.is_active = got_activated;
|
||||
}
|
||||
|
||||
if focused {
|
||||
ws.is_focused = got_activated;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
let ws = self.workspaces.get_mut(&workspace_id);
|
||||
let ws = ws.expect("changed workspace was missing from the map");
|
||||
ws.active_window_id = active_window_id;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WindowsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let windows = self.windows.values().cloned().collect();
|
||||
vec![Event::WindowsChanged { windows }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WindowsChanged { windows } => {
|
||||
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
let (id, is_focused) = match self.windows.entry(window.id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
*entry = window;
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let entry = entry.insert(window);
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
};
|
||||
|
||||
if is_focused {
|
||||
for win in self.windows.values_mut() {
|
||||
if win.id != id {
|
||||
win.is_focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
let win = self.windows.remove(&id);
|
||||
win.expect("closed window was missing from the map");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
for win in self.windows.values_mut() {
|
||||
win.is_focused = Some(win.id) == id;
|
||||
}
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for KeyboardLayoutsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
|
||||
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
self.keyboard_layouts = Some(keyboard_layouts);
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
let kb = self.keyboard_layouts.as_mut();
|
||||
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
|
||||
kb.current_idx = idx;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
[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.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.2.0", path = ".." }
|
||||
niri-config = { version = "25.2.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,3 +0,0 @@
|
||||
.anim-control-bar {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::f32::consts::{FRAC_PI_2, PI};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientAngle {
|
||||
angle: f32,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
impl GradientAngle {
|
||||
pub fn new(_args: Args) -> 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 delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
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::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
self.angle - FRAC_PI_2,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use std::f32::consts::{FRAC_PI_4, PI};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::focus_ring::FocusRing;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientArea {
|
||||
progress: f32,
|
||||
border: FocusRing,
|
||||
prev_time: Duration,
|
||||
}
|
||||
|
||||
impl GradientArea {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
let border = FocusRing::new(niri_config::FocusRing {
|
||||
off: false,
|
||||
width: FloatOrInt(1.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
||||
inactive_color: Color::default(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
});
|
||||
|
||||
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 delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
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::from((size.w - a * 2, size.h - b * 2));
|
||||
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
|
||||
|
||||
let g_size = Size::from((
|
||||
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
|
||||
));
|
||||
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
|
||||
let g_size = g_size.to_f64();
|
||||
let mut g_area = Rectangle::new(g_loc, g_size);
|
||||
g_area.loc -= area.loc;
|
||||
|
||||
self.border.update_render_elements(
|
||||
g_size,
|
||||
true,
|
||||
true,
|
||||
Rectangle::default(),
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(renderer, g_loc)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
rv.extend(
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
g_area,
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
FRAC_PI_4,
|
||||
Rectangle::from_size(rect_size).to_f64(),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklab {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklab {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklab {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklabAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklabAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklabAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchDecreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchDecreasing {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Decreasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchDecreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchIncreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchIncreasing {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Increasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchIncreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchLonger {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchLonger {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchLonger {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchShorter {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchShorter {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchShorter {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgb {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgb {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgb {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinear {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinear {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinear {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinearAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinearAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinearAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
|
||||
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::{Physical, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
|
||||
pub struct Layout {
|
||||
output: Output,
|
||||
windows: Vec<TestWindow>,
|
||||
clock: Clock,
|
||||
layout: niri::layout::Layout<TestWindow>,
|
||||
start_time: Duration,
|
||||
steps: HashMap<Duration, DynStepFn>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(args: Args) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
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);
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: String::new(),
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
|
||||
layout.add_output(output.clone());
|
||||
|
||||
let start_time = clock.now_unadjusted();
|
||||
|
||||
Self {
|
||||
output,
|
||||
windows: Vec::new(),
|
||||
clock,
|
||||
layout,
|
||||
start_time,
|
||||
steps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_between(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||
rv.layout.activate_window(&0);
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::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(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::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(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::Auto,
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
mut window: TestWindow,
|
||||
width: Option<PresetSize>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::NextTo(right_of.id()),
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
|
||||
self.steps
|
||||
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Layout {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let mode = Some(Mode {
|
||||
size: Size::from((width, height)),
|
||||
refresh: 60000,
|
||||
});
|
||||
self.output.change_current_state(mode, None, None, None);
|
||||
layer_map_for_output(&self.output).arrange();
|
||||
self.layout.update_output_size(&self.output);
|
||||
for win in &self.windows {
|
||||
if win.communicate() {
|
||||
self.layout.update_window(win.id(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
let now_unadjusted = self.clock.now_unadjusted();
|
||||
let run = self
|
||||
.steps
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|delay| self.start_time + *delay <= now_unadjusted)
|
||||
.collect::<Vec<_>>();
|
||||
for delay in &run {
|
||||
let now = self.start_time + *delay;
|
||||
self.clock.set_unadjusted(now);
|
||||
self.layout.advance_animations();
|
||||
|
||||
let f = self.steps.remove(delay).unwrap();
|
||||
f(self);
|
||||
}
|
||||
|
||||
self.clock.set_unadjusted(now_unadjusted);
|
||||
self.layout.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout.update_render_elements(Some(&self.output));
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
|
||||
pub mod gradient_angle;
|
||||
pub mod gradient_area;
|
||||
pub mod gradient_oklab;
|
||||
pub mod gradient_oklab_alpha;
|
||||
pub mod gradient_oklch_alpha;
|
||||
pub mod gradient_oklch_decreasing;
|
||||
pub mod gradient_oklch_increasing;
|
||||
pub mod gradient_oklch_longer;
|
||||
pub mod gradient_oklch_shorter;
|
||||
pub mod gradient_srgb;
|
||||
pub mod gradient_srgb_alpha;
|
||||
pub mod gradient_srgblinear;
|
||||
pub mod gradient_srgblinear_alpha;
|
||||
pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
pub struct Args {
|
||||
pub size: Size<i32, Logical>,
|
||||
pub clock: Clock,
|
||||
}
|
||||
|
||||
pub trait TestCase {
|
||||
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
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>>>;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri_config::{Color, FloatOrInt};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Tile {
|
||||
window: TestWindow,
|
||||
tile: niri::layout::tile::Tile<TestWindow>,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn freeform_open(args: Args) -> Self {
|
||||
let mut rv = Self::freeform(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn with_window(args: Args, window: TestWindow) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: FloatOrInt(32.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut tile = niri::layout::tile::Tile::new(
|
||||
window.clone(),
|
||||
size.to_f64(),
|
||||
1.,
|
||||
clock,
|
||||
Rc::new(options),
|
||||
);
|
||||
|
||||
tile.request_tile_size(size.to_f64(), false, None);
|
||||
window.communicate();
|
||||
|
||||
Self { window, tile }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let size = Size::from((width, height)).to_f64();
|
||||
self.tile
|
||||
.update_config(size, 1., self.tile.options().clone());
|
||||
self.tile.request_tile_size(size, false, None);
|
||||
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();
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let size = size.to_f64();
|
||||
let tile_size = self.tile.tile_size().to_physical(1.);
|
||||
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
|
||||
|
||||
self.tile.update_render_elements(
|
||||
true,
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
1.,
|
||||
);
|
||||
self.tile
|
||||
.render(renderer, location, true, RenderTarget::Output)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use niri::layout::LayoutElement;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Scale, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Window {
|
||||
window: TestWindow,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window
|
||||
.request_size(Size::from((width, height)), false, false, None);
|
||||
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, size.h - win_size.h))
|
||||
.to_f64()
|
||||
.downscale(2.);
|
||||
|
||||
self.window
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
1.,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::env;
|
||||
|
||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||
use cases::Args;
|
||||
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
|
||||
use gtk::{gdk, gio, glib};
|
||||
use smithay_view::SmithayView;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cases::gradient_angle::GradientAngle;
|
||||
use crate::cases::gradient_area::GradientArea;
|
||||
use crate::cases::gradient_oklab::GradientOklab;
|
||||
use crate::cases::gradient_oklab_alpha::GradientOklabAlpha;
|
||||
use crate::cases::gradient_oklch_alpha::GradientOklchAlpha;
|
||||
use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing;
|
||||
use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing;
|
||||
use crate::cases::gradient_oklch_longer::GradientOklchLonger;
|
||||
use crate::cases::gradient_oklch_shorter::GradientOklchShorter;
|
||||
use crate::cases::gradient_srgb::GradientSrgb;
|
||||
use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha;
|
||||
use crate::cases::gradient_srgblinear::GradientSrgbLinear;
|
||||
use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha;
|
||||
use crate::cases::layout::Layout;
|
||||
use crate::cases::tile::Tile;
|
||||
use crate::cases::window::Window;
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod cases;
|
||||
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();
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
|
||||
struct S {
|
||||
stack: gtk::Stack,
|
||||
anim_adjustment: gtk::Adjustment,
|
||||
}
|
||||
|
||||
impl S {
|
||||
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
|
||||
let view = SmithayView::new(make, &self.anim_adjustment);
|
||||
self.stack.add_titled(&view, None, title);
|
||||
}
|
||||
}
|
||||
|
||||
let s = S {
|
||||
stack: stack.clone(),
|
||||
anim_adjustment: anim_adjustment.clone(),
|
||||
};
|
||||
|
||||
s.add(Window::freeform, "Freeform Window");
|
||||
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");
|
||||
s.add(GradientSrgb::new, "Gradient - Srgb");
|
||||
s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear");
|
||||
s.add(GradientOklab::new, "Gradient - Oklab");
|
||||
s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter");
|
||||
s.add(GradientOklchLonger::new, "Gradient - Oklch Longer");
|
||||
s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing");
|
||||
s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing");
|
||||
s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha");
|
||||
s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha");
|
||||
s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha");
|
||||
s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha");
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use smithay::utils::Size;
|
||||
|
||||
use crate::cases::{Args, TestCase};
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::ptr::null;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use niri::animation::Clock;
|
||||
use niri::render_helpers::{resources, shaders};
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
|
||||
|
||||
struct RendererData {
|
||||
renderer: GlesRenderer,
|
||||
dummy_texture: GlesTexture,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
gl_area: gtk::GLArea,
|
||||
size: Cell<(i32, i32)>,
|
||||
renderer: RefCell<Option<Result<RendererData, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
pub clock: RefCell<Clock>,
|
||||
}
|
||||
|
||||
#[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 RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
} = renderer;
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
let frame_clock = self.obj().frame_clock().unwrap();
|
||||
let time = Duration::from_micros(frame_clock.frame_time() as u64);
|
||||
self.clock.borrow_mut().set_unadjusted(time);
|
||||
|
||||
// Create the test case if missing.
|
||||
let mut case = self.test_case.borrow_mut();
|
||||
let case = case.get_or_insert_with(|| {
|
||||
let make = self.make_test_case.get().unwrap();
|
||||
let args = Args {
|
||||
size: Size::from(size),
|
||||
clock: self.clock.borrow().clone(),
|
||||
};
|
||||
make(args)
|
||||
});
|
||||
|
||||
case.advance_animations(self.clock.borrow_mut().now());
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
|
||||
|
||||
// Fetch GtkGLArea's framebuffer binding.
|
||||
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");
|
||||
|
||||
// This call will already change the framebuffer binding (offscreen elements will bind
|
||||
// intermediate textures during rendering).
|
||||
let elements = case.render(renderer, Size::from(size));
|
||||
|
||||
// HACK: there's currently no way to "just" render into an externally bound framebuffer
|
||||
// (like we have in this case). The render() call requires a valid target. So what
|
||||
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
|
||||
// before rendering.
|
||||
let mut dummy_target = renderer
|
||||
.bind(dummy_texture)
|
||||
.context("error binding dummy texture")?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(&mut dummy_target, rect.size, Transform::Normal)
|
||||
.context("error creating frame")?;
|
||||
|
||||
// Now that render() bound the dummy texture, change the binding underneath it back to
|
||||
// GtkGLArea's framebuffer, to render there instead.
|
||||
frame
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
frame
|
||||
.clear(Color32F::from([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<RendererData> {
|
||||
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 mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
||||
|
||||
let dummy_texture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
|
||||
.context("error creating dummy texture")?;
|
||||
|
||||
resources::init(&mut renderer);
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
Ok(RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||
@extends gtk::Widget;
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
pub fn new<T: TestCase + 'static>(
|
||||
make_test_case: impl Fn(Args) -> T + 'static,
|
||||
anim_adjustment: >k::Adjustment,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder().build();
|
||||
|
||||
let make = move |args| Box::new(make_test_case(args)) as Box<dyn TestCase>;
|
||||
let make_test_case = Box::new(make) as _;
|
||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||
|
||||
anim_adjustment.connect_value_changed({
|
||||
let obj = obj.downgrade();
|
||||
move |adj| {
|
||||
if let Some(obj) = obj.upgrade() {
|
||||
let mut clock = obj.imp().clock.borrow_mut();
|
||||
let instantly = adj.value() == 0.0;
|
||||
let rate = if instantly {
|
||||
1.0
|
||||
} else {
|
||||
1.0 / adj.value().max(0.001)
|
||||
};
|
||||
clock.set_rate(rate);
|
||||
clock.set_complete_instantly(instantly);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obj
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{
|
||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||
LayoutElementRenderSnapshot,
|
||||
};
|
||||
use niri::render_helpers::offscreen::OffscreenData;
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::output::{self, Output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestWindowInner {
|
||||
size: Size<i32, Logical>,
|
||||
requested_size: Option<Size<i32, Logical>>,
|
||||
min_size: Size<i32, Logical>,
|
||||
max_size: Size<i32, Logical>,
|
||||
buffer: SolidColorBuffer,
|
||||
pending_fullscreen: bool,
|
||||
csd_shadow_width: i32,
|
||||
csd_shadow_buffer: SolidColorBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestWindow {
|
||||
id: usize,
|
||||
inner: Rc<RefCell<TestWindowInner>>,
|
||||
}
|
||||
|
||||
impl TestWindow {
|
||||
pub fn freeform(id: usize) -> Self {
|
||||
let size = Size::from((100, 200));
|
||||
let min_size = Size::from((0, 0));
|
||||
let max_size = Size::from((0, 0));
|
||||
let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]);
|
||||
|
||||
Self {
|
||||
id,
|
||||
inner: Rc::new(RefCell::new(TestWindowInner {
|
||||
size,
|
||||
requested_size: None,
|
||||
min_size,
|
||||
max_size,
|
||||
buffer,
|
||||
pending_fullscreen: false,
|
||||
csd_shadow_width: 0,
|
||||
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fixed_size(id: usize) -> Self {
|
||||
let rv = Self::freeform(id);
|
||||
rv.set_min_size((200, 400).into());
|
||||
rv.set_max_size((200, 400).into());
|
||||
rv.set_color([0.88, 0.11, 0.14, 1.]);
|
||||
rv.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn set_min_size(&self, size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().min_size = size;
|
||||
}
|
||||
|
||||
pub fn set_max_size(&self, size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().max_size = size;
|
||||
}
|
||||
|
||||
pub fn set_color(&self, color: [f32; 4]) {
|
||||
self.inner.borrow_mut().buffer.set_color(color);
|
||||
}
|
||||
|
||||
pub fn set_csd_shadow_width(&self, width: i32) {
|
||||
self.inner.borrow_mut().csd_shadow_width = width;
|
||||
}
|
||||
|
||||
pub fn communicate(&self) -> bool {
|
||||
let mut rv = false;
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
let mut new_size = inner.size;
|
||||
|
||||
if let Some(size) = inner.requested_size {
|
||||
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.to_f64());
|
||||
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.to_f64());
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutElement for TestWindow {
|
||||
type Id = usize;
|
||||
|
||||
fn id(&self) -> &Self::Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<i32, Logical> {
|
||||
self.inner.borrow().size
|
||||
}
|
||||
|
||||
fn buf_loc(&self) -> Point<i32, Logical> {
|
||||
(0, 0).into()
|
||||
}
|
||||
|
||||
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
SplitElements {
|
||||
normal: vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location
|
||||
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
popups: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
is_fullscreen: bool,
|
||||
_animate: bool,
|
||||
_transaction: Option<Transaction>,
|
||||
) {
|
||||
self.inner.borrow_mut().requested_size = Some(size);
|
||||
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
self.inner.borrow().min_size
|
||||
}
|
||||
|
||||
fn max_size(&self) -> Size<i32, Logical> {
|
||||
self.inner.borrow().max_size
|
||||
}
|
||||
|
||||
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
|
||||
|
||||
fn has_ssd(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn output_enter(&self, _output: &Output) {}
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
|
||||
|
||||
fn set_activated(&mut self, _active: bool) {}
|
||||
|
||||
fn set_active_in_column(&mut self, _active: bool) {}
|
||||
|
||||
fn set_floating(&mut self, _floating: bool) {}
|
||||
|
||||
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||
|
||||
fn is_ignoring_opacity_window_rule(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
ConfigureIntent::CanSend
|
||||
}
|
||||
|
||||
fn send_pending_configure(&mut self) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
self.inner.borrow().pending_fullscreen
|
||||
}
|
||||
|
||||
fn requested_size(&self) -> Option<Size<i32, Logical>> {
|
||||
self.inner.borrow().requested_size
|
||||
}
|
||||
|
||||
fn is_child_of(&self, _parent: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn refresh(&self) {}
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules {
|
||||
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
|
||||
&EMPTY
|
||||
}
|
||||
|
||||
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
|
||||
None
|
||||
}
|
||||
|
||||
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
|
||||
|
||||
fn cancel_interactive_resize(&mut self) {}
|
||||
|
||||
fn on_commit(&mut self, _serial: Serial) {}
|
||||
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
}
|
||||
}
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
%bcond_without check
|
||||
|
||||
%global cargo_install_lib 0
|
||||
|
||||
# We want panic backtraces to work without installing the debuginfo package,
|
||||
# so we leave the debuginfo in the main binary.
|
||||
%global debug_package %{nil}
|
||||
%global __strip /bin/true
|
||||
|
||||
# To reduce the file size, do some convincing of rust-srpm-macros
|
||||
# to leave alone the chosen debug settings from Cargo.toml.
|
||||
%global rustflags_debuginfo please-remove-me
|
||||
%global build_rustflags %{shrink:
|
||||
-Copt-level=%rustflags_opt_level
|
||||
-Ccodegen-units=%rustflags_codegen_units
|
||||
-Cstrip=none
|
||||
%{expr:0%{?_include_frame_pointers} && ("%{_arch}" != "ppc64le" && "%{_arch}" != "s390x" && "%{_arch}" != "i386") ? "-Cforce-frame-pointers=yes" : ""}
|
||||
-Clink-arg=-Wl,-z,relro
|
||||
-Clink-arg=-Wl,-z,now
|
||||
%[0%{?_package_note_status} ? "-Clink-arg=%_package_note_flags" : ""]
|
||||
--cap-lints=warn
|
||||
}
|
||||
|
||||
# Convince rust-srpm-macros to use Cargo.lock with the Smithay commit.
|
||||
%global __cargo_common_opts %{?_smp_mflags} -Z avoid-dev-deps --locked
|
||||
|
||||
%global version {{{ git_dir_version }}}
|
||||
|
||||
Name: niri
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Scrollable-tiling Wayland compositor
|
||||
|
||||
SourceLicense: GPL-3.0-or-later
|
||||
|
||||
# 0BSD OR MIT OR Apache-2.0
|
||||
# Apache-2.0
|
||||
# Apache-2.0 OR BSL-1.0
|
||||
# Apache-2.0 OR MIT
|
||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||
# BSD-2-Clause
|
||||
# BSD-2-Clause OR Apache-2.0 OR MIT
|
||||
# BSD-3-Clause OR MIT OR Apache-2.0
|
||||
# GPL-3.0-or-later
|
||||
# ISC
|
||||
# MIT
|
||||
# MIT AND (MIT OR Apache-2.0)
|
||||
# MIT OR Apache-2.0
|
||||
# (MIT OR Apache-2.0) AND BSD-3-Clause
|
||||
# (MIT OR Apache-2.0) AND Unicode-3.0
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unicode-3.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
|
||||
BuildRequires: cargo-rpm-macros >= 26
|
||||
BuildRequires: pkgconfig(udev)
|
||||
BuildRequires: pkgconfig(gbm)
|
||||
BuildRequires: pkgconfig(xkbcommon)
|
||||
BuildRequires: wayland-devel
|
||||
BuildRequires: pkgconfig(libinput)
|
||||
BuildRequires: pkgconfig(dbus-1)
|
||||
BuildRequires: pkgconfig(systemd)
|
||||
BuildRequires: pkgconfig(libseat)
|
||||
BuildRequires: pkgconfig(libdisplay-info)
|
||||
BuildRequires: pipewire-devel
|
||||
BuildRequires: pango-devel
|
||||
BuildRequires: cairo-gobject-devel
|
||||
# Needed for pipewire-rs
|
||||
BuildRequires: clang
|
||||
|
||||
Requires: mesa-dri-drivers
|
||||
Requires: mesa-libEGL
|
||||
|
||||
# Portal implementations used by niri
|
||||
Recommends: xdg-desktop-portal-gtk
|
||||
Recommends: xdg-desktop-portal-gnome
|
||||
Recommends: gnome-keyring
|
||||
|
||||
# Suggested utilities, bound in the default config
|
||||
Recommends: alacritty
|
||||
Recommends: fuzzel
|
||||
Recommends: swaylock
|
||||
Recommends: waybar
|
||||
# Suggested utilities
|
||||
Recommends: swaybg
|
||||
Recommends: mako
|
||||
Recommends: swayidle
|
||||
|
||||
%description
|
||||
A scrollable-tiling Wayland compositor.
|
||||
|
||||
Windows are arranged in columns on an infinite strip going to the right.
|
||||
Opening a new window never causes existing windows to resize.
|
||||
|
||||
%prep
|
||||
{{{ git_dir_setup_macro }}}
|
||||
|
||||
%cargo_prep -N
|
||||
|
||||
# We're doing an online build.
|
||||
sed -i 's/^offline = true$//' .cargo/config.toml
|
||||
|
||||
# Final step in leaving alone our debug settings.
|
||||
sed -i 's/.*please-remove-me$//' .cargo/config.toml
|
||||
|
||||
# Set the commit string.
|
||||
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
|
||||
%install
|
||||
%cargo_install
|
||||
|
||||
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
%cargo_test -- --workspace --exclude niri-visual-tests
|
||||
%endif
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%doc resources/default-config.kdl
|
||||
%doc wiki
|
||||
%{_bindir}/niri
|
||||
%{_bindir}/niri-session
|
||||
%{_datadir}/wayland-sessions/niri.desktop
|
||||
%dir %{_datadir}/xdg-desktop-portal
|
||||
%{_datadir}/xdg-desktop-portal/niri-portals.conf
|
||||
%{_userunitdir}/niri.service
|
||||
%{_userunitdir}/niri-shutdown.target
|
||||
|
||||
%changelog
|
||||
{{{ git_dir_changelog }}}
|
||||
|
||||
+119
-381
@@ -1,11 +1,6 @@
|
||||
// This config is in the KDL format: https://kdl.dev
|
||||
// "/-" comments out the following node.
|
||||
// Check the wiki for a full description of the configuration:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
@@ -16,134 +11,71 @@ input {
|
||||
// layout "us,ru"
|
||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||
}
|
||||
|
||||
// You can set the keyboard repeat parameters. The defaults match wlroots and sway.
|
||||
// Delay is in milliseconds before the repeat starts. Rate is in characters per second.
|
||||
// repeat-delay 600
|
||||
// repeat-rate 25
|
||||
|
||||
// Niri can remember the keyboard layout globally (the default) or per-window.
|
||||
// - "global" - layout change is global for all windows.
|
||||
// - "window" - layout is tracked for each window individually.
|
||||
// track-layout "global"
|
||||
}
|
||||
|
||||
// Next sections include libinput settings.
|
||||
// Omitting settings disables them, or leaves them at their default values.
|
||||
touchpad {
|
||||
// off
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
// drag false
|
||||
// drag-lock
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "two-finger"
|
||||
// disabled-on-external-mouse
|
||||
}
|
||||
|
||||
mouse {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "no-scroll"
|
||||
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
|
||||
// existing outputs.
|
||||
map-to-output "eDP-1"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
// Focus windows and outputs automatically when moving the mouse into them.
|
||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||
// focus-follows-mouse max-scroll-amount="0%"
|
||||
// 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
|
||||
// (i.e. logind.conf).
|
||||
// disable-power-key-handling
|
||||
}
|
||||
|
||||
// You can configure outputs by their name, which you can find
|
||||
// by running `niri msg outputs` while inside a niri instance.
|
||||
// You can configure outputs by their name, which you can find with wayland-info(1).
|
||||
// The built-in laptop monitor is usually called "eDP-1".
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
|
||||
// Remember to uncomment the node by removing "/-"!
|
||||
// Remember to uncommend the node by removing "/-"!
|
||||
/-output "eDP-1" {
|
||||
// Uncomment this line to disable this output.
|
||||
// off
|
||||
|
||||
// Scale is a floating-point number, but at the moment only integer values work.
|
||||
scale 2.0
|
||||
|
||||
// Resolution and, optionally, refresh rate of the output.
|
||||
// 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@120.030"
|
||||
|
||||
// You can use integer or fractional scale, for example use 1.5 for 150% scale.
|
||||
scale 2
|
||||
|
||||
// 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"
|
||||
// All valid modes are listed in niri's debug output when an output is connected.
|
||||
mode "1920x1080@144"
|
||||
|
||||
// Position of the output in the global coordinate space.
|
||||
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
|
||||
// The cursor can only move between directly adjacent outputs.
|
||||
// Output scale and rotation has to be taken into account for positioning:
|
||||
// Output scale has to be taken into account for positioning:
|
||||
// outputs are sized in logical, or scaled, pixels.
|
||||
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
|
||||
// so to put another output directly adjacent to it on the right, set its x to 1920.
|
||||
// If the position is unset or results in an overlap, the output is instead placed
|
||||
// It the position is unset or results in an overlap, the output is instead placed
|
||||
// automatically.
|
||||
position x=1280 y=0
|
||||
}
|
||||
|
||||
// Settings that influence how windows are positioned and sized.
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||
layout {
|
||||
// Set gaps around windows in logical pixels.
|
||||
gaps 16
|
||||
|
||||
// When to center a column when changing focus, options are:
|
||||
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||
// or right edge of the screen.
|
||||
// - "always", the focused column will always be centered.
|
||||
// - "on-overflow", focusing a column will center it if it doesn't fit
|
||||
// together with the previously focused column.
|
||||
center-focused-column "never"
|
||||
|
||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||
preset-column-widths {
|
||||
// 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.33333
|
||||
proportion 0.5
|
||||
proportion 0.66667
|
||||
|
||||
// Fixed sets the width in logical pixels exactly.
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
default-column-width { proportion 0.5; }
|
||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||
// default-column-width {}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Alternatively, you can override it with a window rule called
|
||||
// `draw-border-with-background`.
|
||||
|
||||
// You can change how the focus ring looks.
|
||||
focus-ring {
|
||||
// Uncomment this line to disable the focus ring.
|
||||
@@ -152,31 +84,11 @@ layout {
|
||||
// How many logical pixels the ring extends out from the windows.
|
||||
width 4
|
||||
|
||||
// Colors can be set in a variety of ways:
|
||||
// - CSS named colors: "red"
|
||||
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
|
||||
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
|
||||
// Color of the ring on the active monitor: red, green, blue, alpha.
|
||||
active-color 127 200 255 255
|
||||
|
||||
// Color of the ring on the active monitor.
|
||||
active-color "#7fc8ff"
|
||||
|
||||
// Color of the ring on inactive monitors.
|
||||
inactive-color "#505050"
|
||||
|
||||
// You can also use gradients. They take precedence over solid colors.
|
||||
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
|
||||
// The angle is the same as in linear-gradient, and is optional,
|
||||
// defaulting to 180 (top-to-bottom gradient).
|
||||
// You can use any CSS linear-gradient tool on the web to set these up.
|
||||
// Changing the color space is also supported, check the wiki for more info.
|
||||
//
|
||||
// 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"
|
||||
// Color of the ring on inactive monitors: red, green, blue, alpha.
|
||||
inactive-color 80 80 80 255
|
||||
}
|
||||
|
||||
// You can also add a border. It's similar to the focus ring, but always visible.
|
||||
@@ -186,50 +98,31 @@ layout {
|
||||
off
|
||||
|
||||
width 4
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
active-color 255 200 127 255
|
||||
inactive-color 80 80 80 255
|
||||
}
|
||||
|
||||
// You can enable drop shadows for windows.
|
||||
shadow {
|
||||
// Uncomment the next line to enable shadows.
|
||||
// on
|
||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||
preset-column-widths {
|
||||
// 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.5
|
||||
proportion 0.667
|
||||
|
||||
// By default, the shadow draws only around its window, and not behind it.
|
||||
// Uncomment this setting to make the shadow draw behind its window.
|
||||
//
|
||||
// Note that niri has no way of knowing about the CSD window corner
|
||||
// radius. It has to assume that windows have square corners, leading to
|
||||
// shadow artifacts inside the CSD rounded corners. This setting fixes
|
||||
// those artifacts.
|
||||
//
|
||||
// However, instead you may want to set prefer-no-csd and/or
|
||||
// geometry-corner-radius. Then, niri will know the corner radius and
|
||||
// draw the shadow correctly, without having to draw it behind the
|
||||
// window. These will also remove client-side shadows if the window
|
||||
// draws any.
|
||||
//
|
||||
// draw-behind-window true
|
||||
|
||||
// You can change how shadows look. The values below are in logical
|
||||
// pixels and match the CSS box-shadow properties.
|
||||
|
||||
// Softness controls the shadow blur radius.
|
||||
softness 30
|
||||
|
||||
// Spread expands the shadow.
|
||||
spread 5
|
||||
|
||||
// Offset moves the shadow relative to the window.
|
||||
offset x=0 y=5
|
||||
|
||||
// You can also change the shadow color and opacity.
|
||||
color "#0007"
|
||||
// Fixed sets the width in logical pixels exactly.
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
default-column-width { proportion 0.5; }
|
||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||
// default-column-width {}
|
||||
|
||||
// Set gaps around windows in logical pixels.
|
||||
gaps 16
|
||||
|
||||
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||
// Left and right struts will cause the next window to the side to always be visible.
|
||||
@@ -243,80 +136,34 @@ layout {
|
||||
}
|
||||
}
|
||||
|
||||
// Add lines like this to spawn processes at startup.
|
||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||
// which may be more convenient to use.
|
||||
// See the binds section below for more spawn examples.
|
||||
|
||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||
spawn-at-startup "waybar"
|
||||
|
||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||
// If the client will specifically ask for CSD, the request will be honored.
|
||||
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
|
||||
// This option will also fix border/focus ring drawing behind some semitransparent windows.
|
||||
// After enabling or disabling this, you need to restart the apps for this to take effect.
|
||||
// prefer-no-csd
|
||||
|
||||
// You can change the path where screenshots are saved.
|
||||
// A ~ at the front will be expanded to the home directory.
|
||||
// The path is formatted with strftime(3) to give you the screenshot date and time.
|
||||
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||
|
||||
// You can also set this to null to disable saving screenshots to disk.
|
||||
// screenshot-path null
|
||||
|
||||
// Animation settings.
|
||||
// The wiki explains how to configure individual animations:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
|
||||
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
|
||||
cursor {
|
||||
// Change the theme and size of the cursor as well as set the
|
||||
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
||||
// xcursor-theme "default"
|
||||
// xcursor-size 24
|
||||
}
|
||||
|
||||
// Window rules let you adjust behavior for individual windows.
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||
clients {
|
||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||
// If the client will specifically ask for CSD, the request will be honored.
|
||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||
// prefer-no-csd
|
||||
|
||||
// Work around WezTerm's initial configure bug
|
||||
// by setting an empty default-column-width.
|
||||
window-rule {
|
||||
// This regular expression is intentionally made as specific as possible,
|
||||
// since this is the default config, and we want no false positives.
|
||||
// You can get away with just app-id="wezterm" if you want.
|
||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||
default-column-width {}
|
||||
// Add lines like this to spawn processes at startup.
|
||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||
// which may be more convenient to use.
|
||||
// spawn-at-startup "alacritty" "-e" "fish"
|
||||
}
|
||||
|
||||
// Open the Firefox picture-in-picture player as floating by default.
|
||||
window-rule {
|
||||
// This app-id regular expression will work for both:
|
||||
// - host Firefox (app-id is "firefox")
|
||||
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
|
||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||
open-floating true
|
||||
}
|
||||
screenshot-ui {
|
||||
// You can change the path where screenshots are saved.
|
||||
// A ~ at the front will be expanded to the home directory.
|
||||
// The path is formatted with strftime(3) to give you the screenshot date and time.
|
||||
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||
|
||||
// Example: block out two password managers from screen capture.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||
match app-id=r#"^org\.gnome\.World\.Secrets$"#
|
||||
|
||||
block-out-from "screen-capture"
|
||||
|
||||
// Use this instead if you want them visible on third-party screenshot tools.
|
||||
// block-out-from "screencast"
|
||||
}
|
||||
|
||||
// Example: enable rounded corners for all windows.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
// Uncomment this line to disable saving screenshots to disk.
|
||||
// They will only be copied to the clipboard.
|
||||
// disable-saving-to-disk
|
||||
}
|
||||
|
||||
binds {
|
||||
@@ -326,49 +173,35 @@ 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.
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
|
||||
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
||||
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||
|
||||
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
|
||||
// Note: the entire command goes as a single argument in the end.
|
||||
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||
Mod+T { spawn "alacritty"; }
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Mod+Alt+L { spawn "swaylock"; }
|
||||
|
||||
// Example volume keys mappings for PipeWire & WirePlumber.
|
||||
// The allow-when-locked=true property makes them work even when the session is locked.
|
||||
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
||||
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||
|
||||
Mod+Q { close-window; }
|
||||
|
||||
Mod+Left { focus-column-left; }
|
||||
Mod+Down { focus-window-down; }
|
||||
Mod+Up { focus-window-up; }
|
||||
Mod+Right { focus-column-right; }
|
||||
Mod+H { focus-column-left; }
|
||||
Mod+J { focus-window-down; }
|
||||
Mod+K { focus-window-up; }
|
||||
Mod+L { focus-column-right; }
|
||||
Mod+Left { focus-column-left; }
|
||||
Mod+Down { focus-window-down; }
|
||||
Mod+Up { focus-window-up; }
|
||||
Mod+Right { focus-column-right; }
|
||||
|
||||
Mod+Ctrl+Left { move-column-left; }
|
||||
Mod+Ctrl+Down { move-window-down; }
|
||||
Mod+Ctrl+Up { move-window-up; }
|
||||
Mod+Ctrl+Right { move-column-right; }
|
||||
Mod+Ctrl+H { move-column-left; }
|
||||
Mod+Ctrl+J { move-window-down; }
|
||||
Mod+Ctrl+K { move-window-up; }
|
||||
Mod+Ctrl+L { move-column-right; }
|
||||
Mod+Ctrl+Left { move-column-left; }
|
||||
Mod+Ctrl+Down { move-window-down; }
|
||||
Mod+Ctrl+Up { move-window-up; }
|
||||
Mod+Ctrl+Right { move-column-right; }
|
||||
|
||||
// Alternative commands that move across workspaces when reaching
|
||||
// the first or last window in a column.
|
||||
@@ -382,90 +215,38 @@ binds {
|
||||
Mod+Ctrl+Home { move-column-to-first; }
|
||||
Mod+Ctrl+End { move-column-to-last; }
|
||||
|
||||
Mod+Shift+Left { focus-monitor-left; }
|
||||
Mod+Shift+Down { focus-monitor-down; }
|
||||
Mod+Shift+Up { focus-monitor-up; }
|
||||
Mod+Shift+Right { focus-monitor-right; }
|
||||
Mod+Shift+H { focus-monitor-left; }
|
||||
Mod+Shift+J { focus-monitor-down; }
|
||||
Mod+Shift+K { focus-monitor-up; }
|
||||
Mod+Shift+L { focus-monitor-right; }
|
||||
Mod+Shift+Left { focus-monitor-left; }
|
||||
Mod+Shift+Down { focus-monitor-down; }
|
||||
Mod+Shift+Up { focus-monitor-up; }
|
||||
Mod+Shift+Right { focus-monitor-right; }
|
||||
|
||||
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||
Mod+Shift+Ctrl+H { move-window-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+J { move-window-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+K { move-window-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+L { move-window-to-monitor-right; }
|
||||
Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+Down { move-window-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+Up { move-window-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+Right { move-window-to-monitor-right; }
|
||||
|
||||
// Alternatively, there are commands to move just a single window:
|
||||
// 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; }
|
||||
Mod+I { focus-workspace-up; }
|
||||
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
|
||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+Ctrl+U { move-window-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-window-to-workspace-up; }
|
||||
Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
|
||||
Mod+Ctrl+Page_Up { move-window-to-workspace-up; }
|
||||
|
||||
// Alternatively, there are commands to move just a single window:
|
||||
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
|
||||
// ...
|
||||
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
Mod+Shift+U { move-workspace-down; }
|
||||
Mod+Shift+I { move-workspace-up; }
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
|
||||
// You can bind mouse wheel scroll ticks using the following syntax.
|
||||
// These binds will change direction based on the natural-scroll setting.
|
||||
//
|
||||
// To avoid scrolling through workspaces really fast, you can use
|
||||
// the cooldown-ms property. The bind will be rate-limited to this value.
|
||||
// You can set a cooldown on any bind, but it's most useful for the wheel.
|
||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||
|
||||
Mod+WheelScrollRight { focus-column-right; }
|
||||
Mod+WheelScrollLeft { focus-column-left; }
|
||||
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||
|
||||
// Usually scrolling up and down with Shift in applications results in
|
||||
// horizontal scrolling; these binds replicate that.
|
||||
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||
|
||||
// Similarly, you can bind touchpad scroll "ticks".
|
||||
// Touchpad scrolling is continuous, so for these binds it is split into
|
||||
// discrete intervals.
|
||||
// These binds are also affected by touchpad's natural-scroll, so these
|
||||
// example binds are "inverted", since we have natural-scroll enabled for
|
||||
// touchpads by default.
|
||||
// Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
|
||||
// Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
|
||||
|
||||
// You can refer to workspaces by index. However, keep in mind that
|
||||
// niri is a dynamic workspace system, so these commands are kind of
|
||||
// "best effort". Trying to refer to a workspace index bigger than
|
||||
// the current workspace count will instead refer to the bottommost
|
||||
// (empty) workspace.
|
||||
//
|
||||
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
|
||||
// will all refer to the 3rd workspace.
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+2 { focus-workspace 2; }
|
||||
Mod+3 { focus-workspace 3; }
|
||||
@@ -475,43 +256,22 @@ binds {
|
||||
Mod+7 { focus-workspace 7; }
|
||||
Mod+8 { focus-workspace 8; }
|
||||
Mod+9 { focus-workspace 9; }
|
||||
Mod+Ctrl+1 { move-column-to-workspace 1; }
|
||||
Mod+Ctrl+2 { move-column-to-workspace 2; }
|
||||
Mod+Ctrl+3 { move-column-to-workspace 3; }
|
||||
Mod+Ctrl+4 { move-column-to-workspace 4; }
|
||||
Mod+Ctrl+5 { move-column-to-workspace 5; }
|
||||
Mod+Ctrl+6 { move-column-to-workspace 6; }
|
||||
Mod+Ctrl+7 { move-column-to-workspace 7; }
|
||||
Mod+Ctrl+8 { move-column-to-workspace 8; }
|
||||
Mod+Ctrl+9 { move-column-to-workspace 9; }
|
||||
Mod+Ctrl+1 { move-window-to-workspace 1; }
|
||||
Mod+Ctrl+2 { move-window-to-workspace 2; }
|
||||
Mod+Ctrl+3 { move-window-to-workspace 3; }
|
||||
Mod+Ctrl+4 { move-window-to-workspace 4; }
|
||||
Mod+Ctrl+5 { move-window-to-workspace 5; }
|
||||
Mod+Ctrl+6 { move-window-to-workspace 6; }
|
||||
Mod+Ctrl+7 { move-window-to-workspace 7; }
|
||||
Mod+Ctrl+8 { move-window-to-workspace 8; }
|
||||
Mod+Ctrl+9 { move-window-to-workspace 9; }
|
||||
|
||||
// Alternatively, there are commands to move just a single window:
|
||||
// Mod+Ctrl+1 { move-window-to-workspace 1; }
|
||||
|
||||
// Switches focus between the current and the previous workspace.
|
||||
// Mod+Tab { focus-workspace-previous; }
|
||||
|
||||
// The following binds move the focused window in and out of a column.
|
||||
// If the window is alone, they will consume it into the nearby column to the side.
|
||||
// If the window is already in a column, they will expel it out.
|
||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
// Consume one window from the right to the bottom of the focused column.
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
// Expel the bottom window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
// Expand the focused column to space not taken up by other fully visible columns.
|
||||
// Makes the column "fill the rest of the space".
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
|
||||
Mod+C { center-column; }
|
||||
|
||||
// Finer width adjustments.
|
||||
@@ -529,15 +289,6 @@ binds {
|
||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||
|
||||
// Move the focused window between the floating and the tiling layout.
|
||||
Mod+V { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
|
||||
// Toggle tabbed column display mode.
|
||||
// Windows in this column will appear as vertical tabs,
|
||||
// rather than stacked on top of each other.
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
|
||||
// Actions to switch layouts.
|
||||
// Note: if you uncomment these, make sure you do NOT have
|
||||
// a matching layout switch hotkey configured in xkb options above.
|
||||
@@ -550,21 +301,8 @@ binds {
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// Applications such as remote-desktop clients and software KVM switches may
|
||||
// request that niri stops processing the keyboard shortcuts defined here
|
||||
// so they may, for example, forward the key presses as-is to a remote machine.
|
||||
// It's a good idea to bind an escape hatch to toggle the inhibitor,
|
||||
// so a buggy application can't hold your session hostage.
|
||||
//
|
||||
// The allow-inhibiting=false property can be applied to other binds as well,
|
||||
// which ensures niri always processes them, even when an inhibitor is active.
|
||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete { quit; }
|
||||
|
||||
// Powers off the monitors. To turn them back on, do any input like
|
||||
// moving the mouse or pressing any other key.
|
||||
Mod+Shift+P { power-off-monitors; }
|
||||
|
||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
depends-on = dbus
|
||||
after = niri-shutdown
|
||||
chain-to = niri-shutdown
|
||||
options: always-chain
|
||||
@@ -1,3 +0,0 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="mutter_x11_interop">
|
||||
<description summary="X11 interoperability helper">
|
||||
This protocol is intended to be used by the portal backend to map Wayland
|
||||
dialogs as modal dialogs on top of X11 windows.
|
||||
</description>
|
||||
|
||||
<interface name="mutter_x11_interop" version="1">
|
||||
<description summary="X11 interoperability helper"/>
|
||||
|
||||
<request name="destroy" type="destructor"/>
|
||||
|
||||
<request name="set_x11_parent">
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="xwindow" type="uint"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,5 +1,3 @@
|
||||
[preferred]
|
||||
default=gnome;gtk;
|
||||
org.freedesktop.impl.portal.Access=gtk;
|
||||
org.freedesktop.impl.portal.Notification=gtk;
|
||||
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
||||
|
||||
+33
-47
@@ -11,51 +11,37 @@ if [ -n "$SHELL" ] &&
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to detect the service manager that is being used
|
||||
if hash systemctl >/dev/null 2>&1; then
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
elif hash dinitctl >/dev/null 2>&1; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
|
||||
echo "dinit user daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# 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
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of grahical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
|
||||
|
||||
@@ -9,6 +9,5 @@ Wants=xdg-desktop-autostart.target
|
||||
Before=xdg-desktop-autostart.target
|
||||
|
||||
[Service]
|
||||
Slice=session.slice
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/niri --session
|
||||
ExecStart=/usr/bin/niri
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
/// Shareable lazy clock that can change rate.
|
||||
///
|
||||
/// The clock will fetch the time once and then retain it until explicitly cleared with
|
||||
/// [`Clock::clear`].
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Clock {
|
||||
inner: Rc<RefCell<AdjustableClock>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LazyClock {
|
||||
time: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Clock that can adjust its rate.
|
||||
#[derive(Debug)]
|
||||
struct AdjustableClock {
|
||||
inner: LazyClock,
|
||||
current_time: Duration,
|
||||
last_seen_time: Duration,
|
||||
rate: f64,
|
||||
complete_instantly: bool,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
/// Creates a new clock with the given time.
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
let clock = AdjustableClock::new(LazyClock::with_time(time));
|
||||
Self {
|
||||
inner: Rc::new(RefCell::new(clock)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current time.
|
||||
pub fn now(&self) -> Duration {
|
||||
self.inner.borrow_mut().now()
|
||||
}
|
||||
|
||||
/// Returns the underlying time not adjusted for rate change.
|
||||
pub fn now_unadjusted(&self) -> Duration {
|
||||
self.inner.borrow_mut().inner.now()
|
||||
}
|
||||
|
||||
/// Sets the unadjusted clock time.
|
||||
pub fn set_unadjusted(&mut self, time: Duration) {
|
||||
self.inner.borrow_mut().inner.set(time);
|
||||
}
|
||||
|
||||
/// Clears the stored time so it's re-fetched again next.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.borrow_mut().inner.clear();
|
||||
}
|
||||
|
||||
/// Gets the clock rate.
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.inner.borrow().rate()
|
||||
}
|
||||
|
||||
/// Sets the clock rate.
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.inner.borrow_mut().set_rate(rate);
|
||||
}
|
||||
|
||||
/// Returns whether animations should complete instantly.
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.inner.borrow().should_complete_instantly()
|
||||
}
|
||||
|
||||
/// Sets whether animations should complete instantly.
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.inner.borrow_mut().set_complete_instantly(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Clock {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Clock {}
|
||||
|
||||
impl LazyClock {
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
Self { time: Some(time) }
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.time = None;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, time: Duration) {
|
||||
self.time = Some(time);
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
*self.time.get_or_insert_with(get_monotonic_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl AdjustableClock {
|
||||
pub fn new(mut inner: LazyClock) -> Self {
|
||||
let time = inner.now();
|
||||
Self {
|
||||
inner,
|
||||
current_time: time,
|
||||
last_seen_time: time,
|
||||
rate: 1.,
|
||||
complete_instantly: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.rate = rate.clamp(0., 1000.);
|
||||
}
|
||||
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.complete_instantly
|
||||
}
|
||||
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.complete_instantly = value;
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
let time = self.inner.now();
|
||||
|
||||
if self.last_seen_time == time {
|
||||
return self.current_time;
|
||||
}
|
||||
|
||||
if self.last_seen_time < time {
|
||||
let delta = time - self.last_seen_time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_add(delta);
|
||||
} else {
|
||||
let delta = self.last_seen_time - time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_sub(delta);
|
||||
}
|
||||
|
||||
self.last_seen_time = time;
|
||||
self.current_time
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdjustableClock {
|
||||
fn default() -> Self {
|
||||
Self::new(LazyClock::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frozen_clock() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
assert_eq!(clock.now(), Duration::ZERO);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_change() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
clock.set_rate(0.5);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(50));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(150));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(150));
|
||||
assert_eq!(clock.now(), Duration::from_millis(75));
|
||||
|
||||
clock.set_rate(2.0);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(250));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(250));
|
||||
assert_eq!(clock.now(), Duration::from_millis(275));
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||
use keyframe::EasingFunction;
|
||||
|
||||
mod spring;
|
||||
pub use spring::{Spring, SpringParams};
|
||||
|
||||
mod clock;
|
||||
pub use clock::Clock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation {
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
is_off: bool,
|
||||
duration: Duration,
|
||||
/// Time until the animation first reaches `to`.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
clamped_duration: Duration,
|
||||
start_time: Duration,
|
||||
clock: Clock,
|
||||
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 {
|
||||
Linear,
|
||||
EaseOutQuad,
|
||||
EaseOutCubic,
|
||||
EaseOutExpo,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
) -> Self {
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / clock.rate().max(0.001);
|
||||
|
||||
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
if config.off {
|
||||
rv.is_off = true;
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv.replace_config(config);
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn replace_config(&mut self, config: niri_config::Animation) {
|
||||
self.is_off = config.off;
|
||||
if config.off {
|
||||
self.duration = Duration::ZERO;
|
||||
self.clamped_duration = Duration::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
let start_time = self.start_time;
|
||||
|
||||
match config.kind {
|
||||
niri_config::AnimationKind::Spring(p) => {
|
||||
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
||||
|
||||
let spring = Spring {
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params,
|
||||
};
|
||||
*self = Self::spring(self.clock.clone(), spring);
|
||||
}
|
||||
niri_config::AnimationKind::Easing(p) => {
|
||||
*self = Self::ease(
|
||||
self.clock.clone(),
|
||||
self.from,
|
||||
self.to,
|
||||
self.initial_velocity,
|
||||
u64::from(p.duration_ms),
|
||||
Curve::from(p.curve),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.start_time = start_time;
|
||||
}
|
||||
|
||||
/// Restarts the animation using the previous config.
|
||||
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
if self.is_off {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => Self::ease(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
self.duration.as_millis() as u64,
|
||||
curve,
|
||||
),
|
||||
Kind::Spring(spring) => {
|
||||
let spring = Spring {
|
||||
from,
|
||||
to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params: spring.params,
|
||||
};
|
||||
Self::spring(self.clock.clone(), spring)
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let threshold = 0.001; // FIXME
|
||||
Self::decelerate(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ease(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
duration_ms: u64,
|
||||
curve: Curve,
|
||||
) -> Self {
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
let kind = Kind::Easing { curve };
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
// Our current curves never overshoot.
|
||||
clamped_duration: duration,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spring(clock: Clock, spring: Spring) -> Self {
|
||||
let _span = tracy_client::span!("Animation::spring");
|
||||
|
||||
let duration = spring.duration();
|
||||
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||
let kind = Kind::Spring(spring);
|
||||
|
||||
Self {
|
||||
from: spring.from,
|
||||
to: spring.to,
|
||||
initial_velocity: spring.initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decelerate(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
threshold: f64,
|
||||
) -> Self {
|
||||
let duration_s = if initial_velocity == 0. {
|
||||
0.
|
||||
} else {
|
||||
let coeff = 1000. * deceleration_rate.ln();
|
||||
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
|
||||
};
|
||||
let duration = Duration::from_secs_f64(duration_s);
|
||||
|
||||
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
|
||||
|
||||
let kind = Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
};
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration: duration,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn is_clamped_done(&self) -> bool {
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.clamped_duration
|
||||
}
|
||||
|
||||
pub fn value_at(&self, at: Duration) -> f64 {
|
||||
if at <= self.start_time {
|
||||
// Return from when at == start_time so that when the animations are off, the behavior
|
||||
// within a single event loop cycle (i.e. no time had passed since the start of an
|
||||
// animation) matches the behavior when the animations are on.
|
||||
return self.from;
|
||||
} else if self.start_time + self.duration <= at {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
if self.clock.should_complete_instantly() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = at.saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
let passed = passed.as_secs_f64();
|
||||
let total = self.duration.as_secs_f64();
|
||||
let x = (passed / total).clamp(0., 1.);
|
||||
curve.y(x) * (self.to - self.from) + self.from
|
||||
}
|
||||
Kind::Spring(spring) => {
|
||||
let value = spring.value_at(passed);
|
||||
|
||||
// Protect against numerical instability.
|
||||
let range = (self.to - self.from) * 10.;
|
||||
let a = self.from - range;
|
||||
let b = self.to + range;
|
||||
if self.from <= self.to {
|
||||
value.clamp(a, b)
|
||||
} else {
|
||||
value.clamp(b, a)
|
||||
}
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let passed = passed.as_secs_f64();
|
||||
let coeff = 1000. * deceleration_rate.ln();
|
||||
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value_at(self.clock.now())
|
||||
}
|
||||
|
||||
/// Returns a value that stops at the target value after first reaching it.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
pub fn clamped_value(&self) -> f64 {
|
||||
if self.is_clamped_done() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
self.value()
|
||||
}
|
||||
|
||||
pub fn to(&self) -> f64 {
|
||||
self.to
|
||||
}
|
||||
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
|
||||
pub fn start_time(&self) -> Duration {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
pub fn end_time(&self) -> Duration {
|
||||
self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
pub fn offset(&mut self, offset: f64) {
|
||||
self.from += offset;
|
||||
self.to += offset;
|
||||
|
||||
if let Kind::Spring(spring) = &mut self.kind {
|
||||
spring.from += offset;
|
||||
spring.to += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub fn y(self, x: f64) -> f64 {
|
||||
match self {
|
||||
Curve::Linear => x,
|
||||
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
||||
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<niri_config::AnimationCurve> for Curve {
|
||||
fn from(value: niri_config::AnimationCurve) -> Self {
|
||||
match value {
|
||||
niri_config::AnimationCurve::Linear => Curve::Linear,
|
||||
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
|
||||
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
||||
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||
|
||||
// As first ansatz for the overdamped solution,
|
||||
// and general estimation for the oscillating ones
|
||||
// we take the value of the envelope when it's < epsilon.
|
||||
let mut x0 = -self.params.epsilon.ln() / beta;
|
||||
|
||||
// f64::EPSILON is too small for this specific comparison, so we use
|
||||
// f32::EPSILON even though it's doubles.
|
||||
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
|
||||
return Duration::from_secs_f64(x0);
|
||||
}
|
||||
|
||||
// Since the overdamped solution decays way slower than the envelope
|
||||
// we need to use the value of the oscillation itself.
|
||||
// Newton's root finding method is a good candidate in this particular case:
|
||||
// https://en.wikipedia.org/wiki/Newton%27s_method
|
||||
let mut y0 = self.oscillate(x0);
|
||||
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||
|
||||
let mut x1 = (self.to - y0 + m * x0) / m;
|
||||
let mut y1 = self.oscillate(x1);
|
||||
|
||||
let mut i = 0;
|
||||
while (self.to - y1).abs() > self.params.epsilon {
|
||||
if i > 1000 {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
x0 = x1;
|
||||
y0 = y1;
|
||||
|
||||
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||
|
||||
x1 = (self.to - y0 + m * x0) / m;
|
||||
y1 = self.oscillate(x1);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Duration::from_secs_f64(x1)
|
||||
}
|
||||
|
||||
/// Computes and returns the duration until the spring reaches its target position.
|
||||
pub fn clamped_duration(&self) -> Option<Duration> {
|
||||
let beta = self.params.damping / (2. * self.params.mass);
|
||||
|
||||
if beta.abs() <= f64::EPSILON || beta < 0. {
|
||||
return Some(Duration::MAX);
|
||||
}
|
||||
|
||||
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||
return Some(Duration::ZERO);
|
||||
}
|
||||
|
||||
// The first frame is not that important and we avoid finding the trivial 0 for in-place
|
||||
// animations.
|
||||
let mut i = 1u16;
|
||||
let mut y = self.oscillate(f64::from(i) / 1000.);
|
||||
|
||||
while (self.to - self.from > f64::EPSILON && self.to - y > self.params.epsilon)
|
||||
|| (self.from - self.to > f64::EPSILON && y - self.to > self.params.epsilon)
|
||||
{
|
||||
if i > 3000 {
|
||||
return None;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
y = self.oscillate(f64::from(i) / 1000.);
|
||||
}
|
||||
|
||||
Some(Duration::from_millis(u64::from(i)))
|
||||
}
|
||||
|
||||
/// Returns the spring position at a given time in seconds.
|
||||
fn oscillate(&self, t: f64) -> f64 {
|
||||
let b = self.params.damping;
|
||||
let m = self.params.mass;
|
||||
let k = self.params.stiffness;
|
||||
let v0 = self.initial_velocity;
|
||||
|
||||
let beta = b / (2. * m);
|
||||
let omega0 = (k / m).sqrt();
|
||||
|
||||
let x0 = self.from - self.to;
|
||||
|
||||
let envelope = (-beta * t).exp();
|
||||
|
||||
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
|
||||
// for the differential equation m*ẍ+b*ẋ+kx = 0
|
||||
|
||||
// f64::EPSILON is too small for this specific comparison, so we use
|
||||
// f32::EPSILON even though it's doubles.
|
||||
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
|
||||
// Critically damped.
|
||||
self.to + envelope * (x0 + (beta * x0 + v0) * t)
|
||||
} else if beta < omega0 {
|
||||
// Underdamped.
|
||||
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
|
||||
|
||||
self.to
|
||||
+ envelope
|
||||
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
|
||||
} else {
|
||||
// Overdamped.
|
||||
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
|
||||
|
||||
self.to
|
||||
+ envelope
|
||||
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_equal_from_to_nan() {
|
||||
let spring = Spring {
|
||||
from: 0.,
|
||||
to: 0.,
|
||||
initial_velocity: 0.,
|
||||
params: SpringParams::new(1.15, 850., 0.0001),
|
||||
};
|
||||
let _ = spring.duration();
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
//! Headless backend for tests.
|
||||
//!
|
||||
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
|
||||
//! crucial parts like rendering.
|
||||
|
||||
use std::mem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use niri_config::OutputName;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::element::RenderElementStates;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::utils::Size;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
|
||||
pub struct Headless {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
impl Headless {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ipc_outputs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, _niri: &mut Niri) {}
|
||||
|
||||
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
|
||||
let connector = format!("headless-{n}");
|
||||
let make = "niri".to_string();
|
||||
let model = "headless".to_string();
|
||||
let serial = n.to_string();
|
||||
|
||||
let output = Output::new(
|
||||
connector.clone(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: make.clone(),
|
||||
model: model.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let mode = Mode {
|
||||
size: Size::from((i32::from(size.0), i32::from(size.1))),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector,
|
||||
make: Some(make),
|
||||
model: Some(model),
|
||||
serial: Some(serial),
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
self.ipc_outputs.lock().unwrap().insert(
|
||||
OutputId::next(),
|
||||
niri_ipc::Output {
|
||||
name: output.name(),
|
||||
make: physical_properties.make,
|
||||
model: physical_properties.model,
|
||||
serial: None,
|
||||
physical_size: None,
|
||||
modes: vec![niri_ipc::Mode {
|
||||
width: size.0,
|
||||
height: size.1,
|
||||
refresh_rate: 60_000,
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
},
|
||||
);
|
||||
|
||||
niri.add_output(output, None, false);
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"headless".to_owned()
|
||||
}
|
||||
|
||||
pub fn with_primary_renderer<T>(
|
||||
&mut self,
|
||||
_f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
|
||||
let states = RenderElementStates::default();
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
Refresh::Unknown,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
RedrawState::Queued => (),
|
||||
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||
}
|
||||
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
// FIXME: request redraw on unfinished animations remain
|
||||
|
||||
RenderResult::Submitted
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||
self.ipc_outputs.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Headless {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+11
-82
@@ -2,14 +2,13 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Config, ModKey};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::niri::Niri;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::input::CompositorMod;
|
||||
use crate::Niri;
|
||||
|
||||
pub mod tty;
|
||||
pub use tty::Tty;
|
||||
@@ -17,13 +16,9 @@ pub use tty::Tty;
|
||||
pub mod winit;
|
||||
pub use winit::Winit;
|
||||
|
||||
pub mod headless;
|
||||
pub use headless::Headless;
|
||||
|
||||
pub enum Backend {
|
||||
Tty(Tty),
|
||||
Winit(Winit),
|
||||
Headless(Headless),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -36,29 +31,11 @@ pub enum RenderResult {
|
||||
Skipped,
|
||||
}
|
||||
|
||||
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
|
||||
|
||||
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct OutputId(u64);
|
||||
|
||||
impl OutputId {
|
||||
fn next() -> OutputId {
|
||||
OutputId(OUTPUT_ID_COUNTER.next())
|
||||
}
|
||||
|
||||
pub fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.init(niri),
|
||||
Backend::Winit(winit) => winit.init(niri),
|
||||
Backend::Headless(headless) => headless.init(niri),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +43,6 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.seat_name(),
|
||||
Backend::Winit(winit) => winit.seat_name(),
|
||||
Backend::Headless(headless) => headless.seat_name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +53,6 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
||||
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
||||
Backend::Headless(headless) => headless.with_primary_renderer(f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,20 +65,13 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
||||
Backend::Winit(winit) => winit.render(niri, output),
|
||||
Backend::Headless(headless) => headless.render(niri, output),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mod_key(&self, config: &Config) -> ModKey {
|
||||
pub fn mod_key(&self) -> CompositorMod {
|
||||
match self {
|
||||
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
|
||||
if let Some(ModKey::Alt) = config.input.mod_key {
|
||||
ModKey::Super
|
||||
} else {
|
||||
ModKey::Alt
|
||||
}
|
||||
}),
|
||||
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
|
||||
Backend::Tty(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => CompositorMod::Alt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +79,6 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.change_vt(vt),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +86,6 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.suspend(),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,15 +93,13 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
||||
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,15 +107,14 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.early_import(surface),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||
#[cfg_attr(not(feature = "dbus"), allow(unused))]
|
||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||
Backend::Headless(headless) => headless.ipc_outputs(),
|
||||
Backend::Tty(tty) => tty.connectors(),
|
||||
Backend::Winit(winit) => winit.connectors(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,39 +126,13 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||
Backend::Winit(_) => None,
|
||||
Backend::Headless(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_monitors_active(&mut self, active: bool) {
|
||||
pub fn set_monitors_active(&self, active: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
|
||||
if let Self::Tty(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,12 +151,4 @@ impl Backend {
|
||||
panic!("backend is not Winit")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headless(&mut self) -> &mut Headless {
|
||||
if let Self::Headless(v) = self {
|
||||
v
|
||||
} else {
|
||||
panic!("backend is not Headless")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+406
-1551
File diff suppressed because it is too large
Load Diff
+65
-121
@@ -3,44 +3,48 @@ use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
use smithay::backend::winit::{self, WinitEvent, WinitGraphicsBackend};
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
|
||||
use smithay::reexports::calloop::LoopHandle;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
use smithay::reexports::winit::window::WindowBuilder;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
use super::RenderResult;
|
||||
use crate::config::Config;
|
||||
use crate::niri::{RedrawState, State};
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::Niri;
|
||||
|
||||
pub struct Winit {
|
||||
config: Rc<RefCell<Config>>,
|
||||
output: Output,
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
|
||||
impl Winit {
|
||||
pub fn new(
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<State>,
|
||||
) -> Result<Self, winit::Error> {
|
||||
let builder = Window::default_attributes()
|
||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
|
||||
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_attributes(builder)?;
|
||||
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
||||
|
||||
let output_config = config
|
||||
.borrow()
|
||||
.output
|
||||
.get("winit")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let output = Output::new(
|
||||
"winit".to_string(),
|
||||
@@ -56,36 +60,18 @@ impl Winit {
|
||||
size: backend.window_size(),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
let scale = output_config.scale.clamp(1., 10.).ceil() as i32;
|
||||
output.change_current_state(
|
||||
Some(mode),
|
||||
Some(Transform::Flipped180),
|
||||
Some(Scale::Integer(scale)),
|
||||
None,
|
||||
);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: "winit".to_string(),
|
||||
make: Some("Smithay".to_string()),
|
||||
model: Some("Winit".to_string()),
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
|
||||
OutputId::next(),
|
||||
niri_ipc::Output {
|
||||
name: output.name(),
|
||||
make: physical_properties.make,
|
||||
model: physical_properties.model,
|
||||
serial: None,
|
||||
physical_size: None,
|
||||
modes: vec![niri_ipc::Mode {
|
||||
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
||||
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
|
||||
refresh_rate: 60_000,
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
},
|
||||
let connectors = Arc::new(Mutex::new(HashMap::from([(
|
||||
"winit".to_owned(),
|
||||
output.clone(),
|
||||
)])));
|
||||
|
||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
@@ -103,62 +89,39 @@ impl Winit {
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
{
|
||||
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
|
||||
let output = ipc_outputs.values_mut().next().unwrap();
|
||||
let mode = &mut output.modes[0];
|
||||
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
|
||||
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
|
||||
if let Some(logical) = output.logical.as_mut() {
|
||||
logical.width = size.w as u32;
|
||||
logical.height = size.h as u32;
|
||||
}
|
||||
state.niri.ipc_outputs_changed = true;
|
||||
}
|
||||
|
||||
state.niri.output_resized(&winit.output);
|
||||
state.niri.output_resized(winit.output.clone());
|
||||
}
|
||||
WinitEvent::Input(event) => state.process_input_event(event),
|
||||
WinitEvent::Focus(_) => (),
|
||||
WinitEvent::Redraw => state.niri.queue_redraw(&state.backend.winit().output),
|
||||
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
|
||||
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);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
config,
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
ipc_outputs,
|
||||
})
|
||||
connectors,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
if let Err(err) = self
|
||||
.backend
|
||||
.renderer()
|
||||
.bind_wl_display(&niri.display_handle)
|
||||
{
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
}
|
||||
|
||||
resources::init(renderer);
|
||||
shaders::init(renderer);
|
||||
|
||||
let config = self.config.borrow();
|
||||
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
|
||||
shaders::set_custom_resize_program(renderer, Some(src));
|
||||
}
|
||||
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
|
||||
shaders::set_custom_close_program(renderer, Some(src));
|
||||
}
|
||||
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
|
||||
shaders::set_custom_open_program(renderer, Some(src));
|
||||
}
|
||||
drop(config);
|
||||
|
||||
niri.update_shaders();
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
niri.add_output(self.output.clone(), None);
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
@@ -176,30 +139,15 @@ impl Winit {
|
||||
let _span = tracy_client::span!("Winit::render");
|
||||
|
||||
// Render the elements.
|
||||
let mut elements = niri.render::<GlesRenderer>(
|
||||
self.backend.renderer(),
|
||||
output,
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
// Visualize the damage, if enabled.
|
||||
if niri.debug_draw_damage {
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
||||
}
|
||||
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
|
||||
|
||||
// Hand them over to winit.
|
||||
let res = {
|
||||
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
|
||||
// FIXME: currently impossible to call due to a mutable borrow.
|
||||
//
|
||||
// let age = self.backend.buffer_age().unwrap();
|
||||
let age = 0;
|
||||
self.damage_tracker
|
||||
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
|
||||
.unwrap()
|
||||
};
|
||||
self.backend.bind().unwrap();
|
||||
let age = self.backend.buffer_age().unwrap();
|
||||
let res = self
|
||||
.damage_tracker
|
||||
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
|
||||
.unwrap();
|
||||
|
||||
niri.update_primary_scanout_output(output, &res.states);
|
||||
|
||||
@@ -212,17 +160,17 @@ impl Winit {
|
||||
.wait_for_frame_completion_before_queueing
|
||||
{
|
||||
let _span = tracy_client::span!("wait for completion");
|
||||
if let Err(err) = res.sync.wait() {
|
||||
warn!("error waiting for frame completion: {err:?}");
|
||||
}
|
||||
res.sync.wait();
|
||||
}
|
||||
|
||||
self.backend.submit(Some(damage)).unwrap();
|
||||
self.backend.submit(Some(&damage)).unwrap();
|
||||
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
||||
let mode = output.current_mode().unwrap();
|
||||
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
Refresh::Unknown,
|
||||
refresh,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
@@ -235,16 +183,12 @@ impl Winit {
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
RedrawState::Queued => (),
|
||||
RedrawState::Queued(_) => (),
|
||||
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||
}
|
||||
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
|
||||
// right away instead.
|
||||
if output_state.unfinished_animations_remain {
|
||||
self.backend.window().request_redraw();
|
||||
}
|
||||
@@ -257,17 +201,17 @@ impl Winit {
|
||||
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
||||
Ok(_texture) => true,
|
||||
Ok(_texture) => Ok(()),
|
||||
Err(err) => {
|
||||
debug!("error importing dmabuf: {err:?}");
|
||||
false
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||
self.ipc_outputs.clone()
|
||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||
self.connectors.clone()
|
||||
}
|
||||
}
|
||||
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use niri_ipc::{Action, OutputAction};
|
||||
|
||||
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`).
|
||||
///
|
||||
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
|
||||
/// command line argument takes precedence.
|
||||
#[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 {
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
msg: Msg,
|
||||
/// Format output as JSON.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
///
|
||||
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
|
||||
/// command line argument takes precedence.
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
/// Generate shell completions.
|
||||
Completions { shell: Shell },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Msg {
|
||||
/// List connected outputs.
|
||||
Outputs,
|
||||
/// List workspaces.
|
||||
Workspaces,
|
||||
/// List open windows.
|
||||
Windows,
|
||||
/// List open layer-shell surfaces.
|
||||
Layers,
|
||||
/// Get the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Print information about the focused output.
|
||||
FocusedOutput,
|
||||
/// Print information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Pick a window with the mouse and print information about it.
|
||||
PickWindow,
|
||||
/// Pick a color from the screen with the mouse.
|
||||
PickColor,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
action: Action,
|
||||
},
|
||||
/// Change output configuration temporarily.
|
||||
///
|
||||
/// The configuration is changed temporarily and not saved into the config file. If the output
|
||||
/// configuration subsequently changes in the config file, these temporary changes will be
|
||||
/// forgotten.
|
||||
Output {
|
||||
/// Output name.
|
||||
///
|
||||
/// Run `niri msg outputs` to see the output names.
|
||||
#[arg()]
|
||||
output: String,
|
||||
/// Configuration to apply.
|
||||
#[command(subcommand)]
|
||||
action: OutputAction,
|
||||
},
|
||||
/// Start continuously receiving events from the compositor.
|
||||
EventStream,
|
||||
/// Print the version of the running niri instance.
|
||||
Version,
|
||||
/// Request an error from the running niri instance.
|
||||
RequestError,
|
||||
}
|
||||
+856
@@ -0,0 +1,856 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use bitflags::bitflags;
|
||||
use directories::ProjectDirs;
|
||||
use serde::Deserialize;
|
||||
use serde_with::DeserializeFromStr;
|
||||
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
|
||||
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
||||
use smithay::input::keyboard::{Keysym, XkbConfig};
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub input: Input,
|
||||
#[serde(default)]
|
||||
pub output: HashMap<String, Output>,
|
||||
#[serde(default)]
|
||||
pub layout: Layout,
|
||||
#[serde(default)]
|
||||
pub clients: Clients,
|
||||
#[serde(default)]
|
||||
pub cursor: Cursor,
|
||||
#[serde(default)]
|
||||
pub screenshot_ui: ScreenshotUi,
|
||||
// Running niri without binds doesn't make much sense.
|
||||
pub binds: HashMap<Key, Action>,
|
||||
#[serde(default)]
|
||||
pub debug: DebugConfig,
|
||||
}
|
||||
|
||||
// FIXME: Add other devices.
|
||||
#[derive(Deserialize, Debug, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Input {
|
||||
pub keyboard: Keyboard,
|
||||
pub touchpad: Touchpad,
|
||||
pub tablet: Tablet,
|
||||
pub disable_power_key_handling: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Keyboard {
|
||||
pub xkb: Xkb,
|
||||
pub repeat_delay: u16,
|
||||
pub repeat_rate: u8,
|
||||
pub track_layout: TrackLayout,
|
||||
}
|
||||
|
||||
impl Default for Keyboard {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
xkb: Default::default(),
|
||||
// The defaults were chosen to match wlroots and sway.
|
||||
repeat_delay: 600,
|
||||
repeat_rate: 25,
|
||||
track_layout: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Xkb {
|
||||
pub rules: String,
|
||||
pub model: String,
|
||||
pub layout: String,
|
||||
pub variant: String,
|
||||
pub options: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Xkb {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rules: String::new(),
|
||||
model: String::new(),
|
||||
layout: String::from("us"),
|
||||
variant: String::new(),
|
||||
options: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Xkb {
|
||||
pub fn to_xkb_config(&self) -> XkbConfig {
|
||||
XkbConfig {
|
||||
rules: &self.rules,
|
||||
model: &self.model,
|
||||
layout: &self.layout,
|
||||
variant: &self.variant,
|
||||
options: self.options.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TrackLayout {
|
||||
/// The layout change is global.
|
||||
#[default]
|
||||
Global,
|
||||
/// The layout change is window local.
|
||||
Window,
|
||||
}
|
||||
|
||||
// FIXME: Add the rest of the settings.
|
||||
#[derive(Deserialize, Debug, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Touchpad {
|
||||
pub tap: bool,
|
||||
pub natural_scroll: bool,
|
||||
pub accel_speed: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Tablet {
|
||||
pub map_to_output: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Output {
|
||||
pub off: bool,
|
||||
pub scale: f64,
|
||||
pub position: Option<Position>,
|
||||
pub mode: Option<Mode>,
|
||||
}
|
||||
|
||||
impl Default for Output {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
scale: 1.,
|
||||
position: None,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Position {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromStr, Debug, Clone, PartialEq)]
|
||||
pub struct Mode {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub refresh: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Layout {
|
||||
pub focus_ring: FocusRing,
|
||||
pub border: FocusRing,
|
||||
pub preset_column_widths: Vec<PresetWidth>,
|
||||
// TODO
|
||||
pub default_column_width: Option<PresetWidth>,
|
||||
pub gaps: u16,
|
||||
pub struts: Struts,
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus_ring: Default::default(),
|
||||
border: default_border(),
|
||||
preset_column_widths: vec![
|
||||
PresetWidth::Proportion(0.333),
|
||||
PresetWidth::Proportion(0.5),
|
||||
PresetWidth::Proportion(0.667),
|
||||
],
|
||||
default_column_width: Some(PresetWidth::Proportion(0.5)),
|
||||
gaps: 16,
|
||||
struts: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct FocusRing {
|
||||
pub off: bool,
|
||||
pub width: u16,
|
||||
pub active_color: Color,
|
||||
pub inactive_color: Color,
|
||||
}
|
||||
|
||||
impl Default for FocusRing {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
width: 4,
|
||||
active_color: Color::new(127, 200, 255, 255),
|
||||
inactive_color: Color::new(80, 80, 80, 255),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_border() -> FocusRing {
|
||||
FocusRing {
|
||||
off: true,
|
||||
width: 4,
|
||||
active_color: Color::new(255, 200, 127, 255),
|
||||
inactive_color: Color::new(80, 80, 80, 255),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(from = "[u8; 4]")]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for [f32; 4] {
|
||||
fn from(c: Color) -> Self {
|
||||
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 4]> for Color {
|
||||
fn from(value: [u8; 4]) -> Self {
|
||||
let [r, g, b, a] = value;
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Clients {
|
||||
pub prefer_no_csd: bool,
|
||||
pub spawn_at_startup: Vec<SpawnAtStartup>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SpawnAtStartup {
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Cursor {
|
||||
pub xcursor_theme: String,
|
||||
pub xcursor_size: u8,
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
xcursor_theme: String::from("default"),
|
||||
xcursor_size: 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PresetWidth {
|
||||
Proportion(f64),
|
||||
Fixed(i32),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Struts {
|
||||
pub left: u16,
|
||||
pub right: u16,
|
||||
pub top: u16,
|
||||
pub bottom: u16,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct ScreenshotUi {
|
||||
pub disable_saving_to_disk: bool,
|
||||
pub screenshot_path: String,
|
||||
}
|
||||
|
||||
impl Default for ScreenshotUi {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
disable_saving_to_disk: false,
|
||||
screenshot_path: String::from(
|
||||
"~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromStr, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Key {
|
||||
pub keysym: Keysym,
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Modifiers : u8 {
|
||||
const CTRL = 1;
|
||||
const SHIFT = 2;
|
||||
const ALT = 4;
|
||||
const SUPER = 8;
|
||||
const COMPOSITOR = 16;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Action {
|
||||
Quit,
|
||||
#[serde(skip)]
|
||||
ChangeVt(i32),
|
||||
Suspend,
|
||||
PowerOffMonitors,
|
||||
ToggleDebugTint,
|
||||
Spawn(Vec<String>),
|
||||
#[serde(skip)]
|
||||
ConfirmScreenshot,
|
||||
#[serde(skip)]
|
||||
CancelScreenshot,
|
||||
Screenshot,
|
||||
ScreenshotScreen,
|
||||
ScreenshotWindow,
|
||||
CloseWindow,
|
||||
FullscreenWindow,
|
||||
FocusColumnLeft,
|
||||
FocusColumnRight,
|
||||
FocusColumnFirst,
|
||||
FocusColumnLast,
|
||||
FocusWindowDown,
|
||||
FocusWindowUp,
|
||||
FocusWindowOrWorkspaceDown,
|
||||
FocusWindowOrWorkspaceUp,
|
||||
MoveColumnLeft,
|
||||
MoveColumnRight,
|
||||
MoveColumnToFirst,
|
||||
MoveColumnToLast,
|
||||
MoveWindowDown,
|
||||
MoveWindowUp,
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
ConsumeWindowIntoColumn,
|
||||
ExpelWindowFromColumn,
|
||||
CenterColumn,
|
||||
FocusWorkspaceDown,
|
||||
FocusWorkspaceUp,
|
||||
FocusWorkspace(u8),
|
||||
MoveWindowToWorkspaceDown,
|
||||
MoveWindowToWorkspaceUp,
|
||||
MoveWindowToWorkspace(u8),
|
||||
MoveWorkspaceDown,
|
||||
MoveWorkspaceUp,
|
||||
FocusMonitorLeft,
|
||||
FocusMonitorRight,
|
||||
FocusMonitorDown,
|
||||
FocusMonitorUp,
|
||||
MoveWindowToMonitorLeft,
|
||||
MoveWindowToMonitorRight,
|
||||
MoveWindowToMonitorDown,
|
||||
MoveWindowToMonitorUp,
|
||||
SetWindowHeight(SizeChange),
|
||||
SwitchPresetColumnWidth,
|
||||
MaximizeColumn,
|
||||
SetColumnWidth(SizeChange),
|
||||
SwitchLayout(LayoutAction),
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromStr, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SizeChange {
|
||||
SetFixed(i32),
|
||||
SetProportion(f64),
|
||||
AdjustFixed(i32),
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LayoutAction {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct DebugConfig {
|
||||
pub animation_slowdown: f64,
|
||||
pub dbus_interfaces_in_non_session_instances: bool,
|
||||
pub wait_for_frame_completion_before_queueing: bool,
|
||||
pub enable_color_transformations_capability: bool,
|
||||
pub enable_overlay_planes: bool,
|
||||
pub disable_cursor_plane: bool,
|
||||
pub render_drm_device: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for DebugConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
animation_slowdown: 1.,
|
||||
dbus_interfaces_in_non_session_instances: false,
|
||||
wait_for_frame_completion_before_queueing: false,
|
||||
enable_color_transformations_capability: false,
|
||||
enable_overlay_planes: false,
|
||||
disable_cursor_plane: false,
|
||||
render_drm_device: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: Option<PathBuf>) -> anyhow::Result<(Self, PathBuf)> {
|
||||
let path = if let Some(path) = path {
|
||||
path
|
||||
} else {
|
||||
let mut path = ProjectDirs::from("", "", "niri")
|
||||
.context("error retrieving home directory")?
|
||||
.config_dir()
|
||||
.to_owned();
|
||||
path.push("config.kdl");
|
||||
path
|
||||
};
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(&path).with_context(|| format!("error reading {path:?}"))?;
|
||||
|
||||
let config = Self::parse(&contents).context("error parsing")?;
|
||||
debug!("loaded config from {path:?}");
|
||||
Ok((config, path))
|
||||
}
|
||||
|
||||
pub fn parse(text: &str) -> Result<Self, toml::de::Error> {
|
||||
toml::from_str(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config::parse(include_str!("../resources/default-config.kdl"))
|
||||
.context("error parsing default config")
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (width, rest) = s.split_once('x').context("no 'x' separator found")?;
|
||||
|
||||
let (height, refresh) = match rest.split_once('@') {
|
||||
Some((height, refresh)) => (height, Some(refresh)),
|
||||
None => (rest, None),
|
||||
};
|
||||
|
||||
let width = width.parse().context("error parsing width")?;
|
||||
let height = height.parse().context("error parsing height")?;
|
||||
let refresh = refresh
|
||||
.map(str::parse)
|
||||
.transpose()
|
||||
.context("error parsing refresh rate")?;
|
||||
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Key {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
|
||||
let mut split = s.split('+');
|
||||
let key = split.next_back().unwrap();
|
||||
|
||||
for part in split {
|
||||
let part = part.trim();
|
||||
if part.eq_ignore_ascii_case("mod") {
|
||||
modifiers |= Modifiers::COMPOSITOR
|
||||
} else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") {
|
||||
modifiers |= Modifiers::CTRL;
|
||||
} else if part.eq_ignore_ascii_case("shift") {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
} else if part.eq_ignore_ascii_case("alt") {
|
||||
modifiers |= Modifiers::ALT;
|
||||
} else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") {
|
||||
modifiers |= Modifiers::SUPER;
|
||||
} else {
|
||||
bail!("invalid modifier: {part}");
|
||||
}
|
||||
}
|
||||
|
||||
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
|
||||
if keysym.raw() == KEY_NoSymbol {
|
||||
bail!("invalid key: {key}");
|
||||
}
|
||||
|
||||
Ok(Key { keysym, modifiers })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SizeChange {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once('%') {
|
||||
Some((value, empty)) => {
|
||||
if !empty.is_empty() {
|
||||
bail!("trailing characters after '%' are not allowed");
|
||||
}
|
||||
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().context("error parsing value")?;
|
||||
Ok(Self::AdjustProportion(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().context("error parsing value")?;
|
||||
Ok(Self::SetProportion(value))
|
||||
}
|
||||
None => bail!("value is missing"),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let value = s;
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().context("error parsing value")?;
|
||||
Ok(Self::AdjustFixed(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().context("error parsing value")?;
|
||||
Ok(Self::SetFixed(value))
|
||||
}
|
||||
None => bail!("value is missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn check(text: &str, expected: Config) {
|
||||
let parsed = Config::parse(text).map_err(anyhow::Error::new).unwrap();
|
||||
assert_eq!(parsed, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
check(
|
||||
r#"
|
||||
[input]
|
||||
disable_power_key_handling = true
|
||||
|
||||
[input.keyboard]
|
||||
repeat_delay = 600
|
||||
repeat_rate = 25
|
||||
track_layout = 'window'
|
||||
xkb.layout = 'us,ru'
|
||||
xkb.options = 'grp:win_space_toggle'
|
||||
|
||||
[input.touchpad]
|
||||
tap = true
|
||||
accel_speed = 0.2
|
||||
|
||||
[input.tablet]
|
||||
map_to_output = 'eDP-1'
|
||||
|
||||
[output.'eDP-1']
|
||||
scale = 2.0
|
||||
position = { x = 10, y = 20 }
|
||||
mode = '1920x1080@144'
|
||||
|
||||
[layout]
|
||||
gaps = 8
|
||||
preset_column_widths = [
|
||||
{ proportion = 0.25 },
|
||||
{ proportion = 0.5 },
|
||||
{ fixed = 960 },
|
||||
{ fixed = 1280 },
|
||||
]
|
||||
default_column_width = { proportion = 0.25 }
|
||||
struts = { left = 1, right = 2, top = 3 }
|
||||
|
||||
[layout.focus_ring]
|
||||
width = 5
|
||||
active_color = [0, 100, 200, 255]
|
||||
inactive_color = [255, 200, 100, 0]
|
||||
|
||||
[layout.border]
|
||||
width = 3
|
||||
active_color = [0, 100, 200, 255]
|
||||
inactive_color = [255, 200, 100, 0]
|
||||
|
||||
[clients]
|
||||
prefer_no_csd = true
|
||||
spawn_at_startup = [
|
||||
{ command = ['alacritty', '-e', 'fish'] },
|
||||
]
|
||||
|
||||
[cursor]
|
||||
xcursor_theme = 'breeze_cursors'
|
||||
xcursor_size = 16
|
||||
|
||||
[screenshot_ui]
|
||||
screenshot_path = '~/Screenshots/screenshot.png'
|
||||
disable_saving_to_disk = true
|
||||
|
||||
[binds]
|
||||
'Mod+T' = { spawn = ['alacritty'] }
|
||||
'Mod+Q' = 'close_window'
|
||||
'Mod+Shift+H' = 'focus_monitor_left'
|
||||
'Mod+Ctrl+Shift+L' = 'move_window_to_monitor_right'
|
||||
'Mod+Comma' = 'consume_window_into_column'
|
||||
'Mod+1' = { focus_workspace = 1 }
|
||||
|
||||
[debug]
|
||||
animation_slowdown = 2.0
|
||||
render_drm_device = '/dev/dri/renderD129'
|
||||
"#,
|
||||
Config {
|
||||
input: Input {
|
||||
keyboard: Keyboard {
|
||||
xkb: Xkb {
|
||||
layout: "us,ru".to_owned(),
|
||||
options: Some("grp:win_space_toggle".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
repeat_delay: 600,
|
||||
repeat_rate: 25,
|
||||
track_layout: TrackLayout::Window,
|
||||
},
|
||||
touchpad: Touchpad {
|
||||
tap: true,
|
||||
natural_scroll: false,
|
||||
accel_speed: 0.2,
|
||||
},
|
||||
tablet: Tablet {
|
||||
map_to_output: Some("eDP-1".to_owned()),
|
||||
},
|
||||
disable_power_key_handling: true,
|
||||
},
|
||||
output: HashMap::from([(
|
||||
"eDP-1".to_owned(),
|
||||
Output {
|
||||
off: false,
|
||||
scale: 2.,
|
||||
position: Some(Position { x: 10, y: 20 }),
|
||||
mode: Some(Mode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh: Some(144.),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
layout: Layout {
|
||||
focus_ring: FocusRing {
|
||||
off: false,
|
||||
width: 5,
|
||||
active_color: Color {
|
||||
r: 0,
|
||||
g: 100,
|
||||
b: 200,
|
||||
a: 255,
|
||||
},
|
||||
inactive_color: Color {
|
||||
r: 255,
|
||||
g: 200,
|
||||
b: 100,
|
||||
a: 0,
|
||||
},
|
||||
},
|
||||
border: FocusRing {
|
||||
off: false,
|
||||
width: 3,
|
||||
active_color: Color {
|
||||
r: 0,
|
||||
g: 100,
|
||||
b: 200,
|
||||
a: 255,
|
||||
},
|
||||
inactive_color: Color {
|
||||
r: 255,
|
||||
g: 200,
|
||||
b: 100,
|
||||
a: 0,
|
||||
},
|
||||
},
|
||||
preset_column_widths: vec![
|
||||
PresetWidth::Proportion(0.25),
|
||||
PresetWidth::Proportion(0.5),
|
||||
PresetWidth::Fixed(960),
|
||||
PresetWidth::Fixed(1280),
|
||||
],
|
||||
default_column_width: Some(PresetWidth::Proportion(0.25)),
|
||||
gaps: 8,
|
||||
struts: Struts {
|
||||
left: 1,
|
||||
right: 2,
|
||||
top: 3,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
clients: Clients {
|
||||
spawn_at_startup: vec![SpawnAtStartup {
|
||||
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
|
||||
}],
|
||||
prefer_no_csd: true,
|
||||
},
|
||||
cursor: Cursor {
|
||||
xcursor_theme: String::from("breeze_cursors"),
|
||||
xcursor_size: 16,
|
||||
},
|
||||
screenshot_ui: ScreenshotUi {
|
||||
disable_saving_to_disk: true,
|
||||
screenshot_path: String::from("~/Screenshots/screenshot.png"),
|
||||
},
|
||||
binds: HashMap::from([
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::t,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
Action::Spawn(vec!["alacritty".to_owned()]),
|
||||
),
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::q,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
Action::CloseWindow,
|
||||
),
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::h,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
|
||||
},
|
||||
Action::FocusMonitorLeft,
|
||||
),
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::l,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
|
||||
},
|
||||
Action::MoveWindowToMonitorRight,
|
||||
),
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::comma,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
Action::ConsumeWindowIntoColumn,
|
||||
),
|
||||
(
|
||||
Key {
|
||||
keysym: Keysym::_1,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
Action::FocusWorkspace(1),
|
||||
),
|
||||
]),
|
||||
debug: DebugConfig {
|
||||
animation_slowdown: 2.,
|
||||
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_create_default_config() {
|
||||
let _ = Config::default();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode() {
|
||||
assert_eq!(
|
||||
"2560x1600@165.004".parse::<Mode>().unwrap(),
|
||||
Mode {
|
||||
width: 2560,
|
||||
height: 1600,
|
||||
refresh: Some(165.004),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"1920x1080".parse::<Mode>().unwrap(),
|
||||
Mode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh: None,
|
||||
},
|
||||
);
|
||||
|
||||
assert!("1920".parse::<Mode>().is_err());
|
||||
assert!("1920x".parse::<Mode>().is_err());
|
||||
assert!("1920x1080@".parse::<Mode>().is_err());
|
||||
assert!("1920x1080@60Hz".parse::<Mode>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_size_change() {
|
||||
assert_eq!(
|
||||
"10".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::SetFixed(10),
|
||||
);
|
||||
assert_eq!(
|
||||
"+10".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::AdjustFixed(10),
|
||||
);
|
||||
assert_eq!(
|
||||
"-10".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::AdjustFixed(-10),
|
||||
);
|
||||
assert_eq!(
|
||||
"10%".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::SetProportion(10.),
|
||||
);
|
||||
assert_eq!(
|
||||
"+10%".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::AdjustProportion(10.),
|
||||
);
|
||||
assert_eq!(
|
||||
"-10%".parse::<SizeChange>().unwrap(),
|
||||
SizeChange::AdjustProportion(-10.),
|
||||
);
|
||||
|
||||
assert!("-".parse::<SizeChange>().is_err());
|
||||
assert!("10% ".parse::<SizeChange>().is_err());
|
||||
}
|
||||
}
|
||||
+28
-23
@@ -4,11 +4,13 @@ use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::backend::renderer::element::texture::TextureBuffer;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
@@ -66,7 +68,7 @@ impl CursorManager {
|
||||
let hotspot = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.get::<Mutex<CursorImageAttributes>>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -75,24 +77,21 @@ impl CursorManager {
|
||||
|
||||
RenderCursor::Surface { hotspot, surface }
|
||||
}
|
||||
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
|
||||
CursorImageStatus::Named(icon) => self
|
||||
.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
|
||||
self.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
|
||||
match &self.current_cursor {
|
||||
CursorImageStatus::Hidden => false,
|
||||
@@ -144,7 +143,7 @@ impl CursorManager {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Currently used cursor_image as a cursor provider.
|
||||
/// Currenly used cursor_image as a cursor provider.
|
||||
pub fn cursor_image(&self) -> &CursorImageStatus {
|
||||
&self.current_cursor
|
||||
}
|
||||
@@ -225,7 +224,7 @@ pub enum RenderCursor {
|
||||
},
|
||||
}
|
||||
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CursorTextureCache {
|
||||
@@ -239,11 +238,12 @@ impl CursorTextureCache {
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
icon: CursorIcon,
|
||||
scale: i32,
|
||||
cursor: &XCursor,
|
||||
idx: usize,
|
||||
) -> MemoryRenderBuffer {
|
||||
) -> TextureBuffer<GlesTexture> {
|
||||
self.cache
|
||||
.borrow_mut()
|
||||
.entry((icon, scale))
|
||||
@@ -252,14 +252,19 @@ impl CursorTextureCache {
|
||||
.frames()
|
||||
.iter()
|
||||
.map(|frame| {
|
||||
MemoryRenderBuffer::from_slice(
|
||||
let _span = tracy_client::span!("create TextureBuffer");
|
||||
|
||||
TextureBuffer::from_memory(
|
||||
renderer,
|
||||
&frame.pixels_rgba,
|
||||
Fourcc::Argb8888,
|
||||
Fourcc::Abgr8888,
|
||||
(frame.width as i32, frame.height as i32),
|
||||
false,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
})[idx]
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
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::message::Header;
|
||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||
use zbus::zvariant::NoneValue;
|
||||
use zbus::{interface, 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<()>>>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
async fn inhibit(
|
||||
&mut self,
|
||||
#[zbus(header)] hdr: Header<'_>,
|
||||
application_name: &str,
|
||||
reason_for_inhibit: &str,
|
||||
) -> fdo::Result<u32> {
|
||||
trace!(
|
||||
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
|
||||
hdr.sender()
|
||||
);
|
||||
|
||||
let 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,82 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{SerializeDict, Type, Value};
|
||||
|
||||
use super::Start;
|
||||
|
||||
pub struct Introspect {
|
||||
to_niri: calloop::channel::Sender<IntrospectToNiri>,
|
||||
from_niri: async_channel::Receiver<NiriToIntrospect>,
|
||||
}
|
||||
|
||||
pub enum IntrospectToNiri {
|
||||
GetWindows,
|
||||
}
|
||||
|
||||
pub enum NiriToIntrospect {
|
||||
Windows(HashMap<u64, WindowProperties>),
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
pub struct WindowProperties {
|
||||
/// Window title.
|
||||
pub title: String,
|
||||
/// Window app ID.
|
||||
///
|
||||
/// This is actually the name of the .desktop file, and Shell does internal tracking to match
|
||||
/// Wayland app IDs to desktop files. We don't do that yet, which is the reason why
|
||||
/// xdg-desktop-portal-gnome's window list is missing icons.
|
||||
#[zvariant(rename = "app-id")]
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Shell.Introspect")]
|
||||
impl Introspect {
|
||||
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
|
||||
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
match self.from_niri.recv().await {
|
||||
Ok(NiriToIntrospect::Windows(windows)) => Ok(windows),
|
||||
Err(err) => {
|
||||
warn!("error receiving message from niri: {err:?}");
|
||||
Err(fdo::Error::Failed("internal error".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
|
||||
// needed for the event stream IPC anyway).
|
||||
#[zbus(signal)]
|
||||
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl Introspect {
|
||||
pub fn new(
|
||||
to_niri: calloop::channel::Sender<IntrospectToNiri>,
|
||||
from_niri: async_channel::Receiver<NiriToIntrospect>,
|
||||
) -> Self {
|
||||
Self { to_niri, from_niri }
|
||||
}
|
||||
}
|
||||
|
||||
impl Start for Introspect {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let conn = zbus::blocking::Connection::session()?;
|
||||
let flags = RequestNameFlags::AllowReplacement
|
||||
| RequestNameFlags::ReplaceExisting
|
||||
| RequestNameFlags::DoNotQueue;
|
||||
|
||||
conn.object_server()
|
||||
.at("/org/gnome/Shell/Introspect", self)?;
|
||||
conn.request_name_with_flags("org.gnome.Shell.Introspect", flags)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use niri_ipc::PickedColor;
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::dbus_interface;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::zvariant::OwnedValue;
|
||||
use zbus::{interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -15,14 +13,13 @@ pub struct Screenshot {
|
||||
|
||||
pub enum ScreenshotToNiri {
|
||||
TakeScreenshot { include_cursor: bool },
|
||||
PickColor(async_channel::Sender<Option<PickedColor>>),
|
||||
}
|
||||
|
||||
pub enum NiriToScreenshot {
|
||||
ScreenshotResult(Option<PathBuf>),
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Shell.Screenshot")]
|
||||
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
|
||||
impl Screenshot {
|
||||
async fn screenshot(
|
||||
&self,
|
||||
@@ -51,34 +48,6 @@ impl Screenshot {
|
||||
|
||||
Ok((true, filename))
|
||||
}
|
||||
|
||||
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
|
||||
warn!("error sending pick color message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
let color = match rx.recv().await {
|
||||
Ok(Some(color)) => color,
|
||||
Ok(None) => {
|
||||
return Err(fdo::Error::Failed("no color picked".to_owned()));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error receiving message from niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = HashMap::new();
|
||||
let [r, g, b] = color.rgb;
|
||||
result.insert(
|
||||
"color".to_string(),
|
||||
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
|
||||
+9
-54
@@ -1,10 +1,9 @@
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::object_server::Interface;
|
||||
use zbus::Interface;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub mod freedesktop_screensaver;
|
||||
pub mod gnome_shell_introspect;
|
||||
pub mod gnome_shell_screenshot;
|
||||
pub mod mutter_display_config;
|
||||
pub mod mutter_service_channel;
|
||||
@@ -14,8 +13,6 @@ pub mod mutter_screen_cast;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
use mutter_screen_cast::ScreenCast;
|
||||
|
||||
use self::freedesktop_screensaver::ScreenSaver;
|
||||
use self::gnome_shell_introspect::Introspect;
|
||||
use self::mutter_display_config::DisplayConfig;
|
||||
use self::mutter_service_channel::ServiceChannel;
|
||||
|
||||
@@ -27,9 +24,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>,
|
||||
pub conn_introspect: Option<Connection>,
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub conn_screen_cast: Option<Connection>,
|
||||
}
|
||||
@@ -45,44 +40,14 @@ impl DBusServers {
|
||||
let mut dbus = Self::default();
|
||||
|
||||
if is_session_instance {
|
||||
let (to_niri, from_service_channel) = calloop::channel::channel();
|
||||
let service_channel = ServiceChannel::new(to_niri);
|
||||
niri.event_loop
|
||||
.insert_source(from_service_channel, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_client) => {
|
||||
state.niri.insert_client(new_client);
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
let service_channel = ServiceChannel::new(niri.display_handle.clone());
|
||||
dbus.conn_service_channel = try_start(service_channel);
|
||||
}
|
||||
|
||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||
let (to_niri, from_display_config) = calloop::channel::channel();
|
||||
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
|
||||
niri.event_loop
|
||||
.insert_source(from_display_config, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_conf) => {
|
||||
for (name, conf) in new_conf {
|
||||
state.modify_output_config(&name, move |output| {
|
||||
if let Some(new_output) = conf {
|
||||
*output = new_output;
|
||||
} else {
|
||||
output.off = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
state.reload_output_config();
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
let display_config = DisplayConfig::new(backend.connectors());
|
||||
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
|
||||
@@ -96,31 +61,21 @@ impl DBusServers {
|
||||
let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri);
|
||||
dbus.conn_screen_shot = try_start(screenshot);
|
||||
|
||||
let (to_niri, from_introspect) = calloop::channel::channel();
|
||||
let (to_introspect, from_niri) = async_channel::unbounded();
|
||||
niri.event_loop
|
||||
.insert_source(from_introspect, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => {
|
||||
state.on_introspect_msg(&to_introspect, msg)
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
let introspect = Introspect::new(to_niri, from_niri);
|
||||
dbus.conn_introspect = try_start(introspect);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
{
|
||||
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
||||
niri.event_loop
|
||||
.insert_source(from_screen_cast, {
|
||||
let to_niri = to_niri.clone();
|
||||
move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg),
|
||||
calloop::channel::Event::Msg(msg) => {
|
||||
state.on_screen_cast_msg(&to_niri, msg)
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
||||
let screen_cast = ScreenCast::new(backend.connectors(), to_niri);
|
||||
dbus.conn_screen_cast = try_start(screen_cast);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smithay::utils::Size;
|
||||
use serde::Serialize;
|
||||
use smithay::output::Output;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
use zbus::{fdo, interface};
|
||||
use zbus::zvariant::{OwnedValue, Type};
|
||||
use zbus::{dbus_interface, fdo};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
use crate::utils::scale::supported_scales;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Type)]
|
||||
@@ -48,18 +42,7 @@ pub struct LogicalMonitor {
|
||||
properties: HashMap<String, OwnedValue>,
|
||||
}
|
||||
|
||||
// ApplyMonitorsConfig
|
||||
#[derive(Deserialize, Type)]
|
||||
pub struct LogicalMonitorConfiguration {
|
||||
x: i32,
|
||||
y: i32,
|
||||
scale: f64,
|
||||
transform: u32,
|
||||
_is_primary: bool,
|
||||
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
impl DisplayConfig {
|
||||
async fn get_current_state(
|
||||
&self,
|
||||
@@ -70,226 +53,40 @@ impl DisplayConfig {
|
||||
HashMap<String, OwnedValue>,
|
||||
)> {
|
||||
// Construct the DBus response.
|
||||
let mut monitors = Vec::new();
|
||||
let mut logical_monitors = Vec::new();
|
||||
let monitors: Vec<Monitor> = self
|
||||
.connectors
|
||||
.lock()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(|c| Monitor {
|
||||
names: (c.clone(), String::new(), String::new(), String::new()),
|
||||
modes: vec![],
|
||||
properties: HashMap::new(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
for output in self.ipc_outputs.lock().unwrap().values() {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
let logical_monitors = monitors
|
||||
.iter()
|
||||
.map(|m| LogicalMonitor {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1.,
|
||||
transform: 0,
|
||||
is_primary: false,
|
||||
monitors: vec![m.names.clone()],
|
||||
properties: HashMap::new(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
);
|
||||
|
||||
let mut modes: Vec<Mode> = output
|
||||
.modes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let niri_ipc::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = *m;
|
||||
let width = i32::from(width);
|
||||
let height = i32::from(height);
|
||||
let refresh_rate = refresh_rate as f64 / 1000.;
|
||||
|
||||
Mode {
|
||||
id: format!("{width}x{height}@{refresh_rate:.3}"),
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
preferred_scale: 1.,
|
||||
supported_scales: supported_scales(Size::from((width, height))).collect(),
|
||||
properties: HashMap::from([(
|
||||
String::from("is-preferred"),
|
||||
OwnedValue::from(is_preferred),
|
||||
)]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if let Some(mode) = output.current_mode {
|
||||
modes[mode]
|
||||
.properties
|
||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||
}
|
||||
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
|
||||
// Serial is used for session restore, so fall back to the connector name if it's
|
||||
// not available.
|
||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||
|
||||
let names = (connector, make, model, serial);
|
||||
|
||||
if let Some(logical) = output.logical.as_ref() {
|
||||
let transform = match logical.transform {
|
||||
niri_ipc::Transform::Normal => 0,
|
||||
niri_ipc::Transform::_90 => 1,
|
||||
niri_ipc::Transform::_180 => 2,
|
||||
niri_ipc::Transform::_270 => 3,
|
||||
niri_ipc::Transform::Flipped => 4,
|
||||
niri_ipc::Transform::Flipped90 => 5,
|
||||
niri_ipc::Transform::Flipped180 => 6,
|
||||
niri_ipc::Transform::Flipped270 => 7,
|
||||
};
|
||||
|
||||
logical_monitors.push(LogicalMonitor {
|
||||
x: logical.x,
|
||||
y: logical.y,
|
||||
scale: logical.scale,
|
||||
transform,
|
||||
is_primary: false,
|
||||
monitors: vec![names.clone()],
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
monitors.push(Monitor {
|
||||
names,
|
||||
modes,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by connector.
|
||||
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
|
||||
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
|
||||
|
||||
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||
Ok((0, monitors, logical_monitors, properties))
|
||||
Ok((0, monitors, logical_monitors, HashMap::new()))
|
||||
}
|
||||
|
||||
async fn apply_monitors_config(
|
||||
&self,
|
||||
_serial: u32,
|
||||
method: u32,
|
||||
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
|
||||
_properties: HashMap<String, OwnedValue>,
|
||||
) -> fdo::Result<()> {
|
||||
let current_conf = self.ipc_outputs.lock().unwrap();
|
||||
let mut new_conf = HashMap::new();
|
||||
for requested_config in logical_monitor_configs {
|
||||
if requested_config.monitors.len() > 1 {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"Mirroring is not yet supported".to_owned(),
|
||||
));
|
||||
}
|
||||
for (connector, mode, _props) in requested_config.monitors {
|
||||
if !current_conf.values().any(|o| o.name == connector) {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Connector '{}' not found",
|
||||
connector
|
||||
)));
|
||||
}
|
||||
new_conf.insert(
|
||||
connector.clone(),
|
||||
Some(niri_config::Output {
|
||||
off: false,
|
||||
name: connector,
|
||||
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
|
||||
transform: match requested_config.transform {
|
||||
0 => niri_ipc::Transform::Normal,
|
||||
1 => niri_ipc::Transform::_90,
|
||||
2 => niri_ipc::Transform::_180,
|
||||
3 => niri_ipc::Transform::_270,
|
||||
4 => niri_ipc::Transform::Flipped,
|
||||
5 => niri_ipc::Transform::Flipped90,
|
||||
6 => niri_ipc::Transform::Flipped180,
|
||||
7 => niri_ipc::Transform::Flipped270,
|
||||
x => {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Unknown transform {}",
|
||||
x
|
||||
)))
|
||||
}
|
||||
},
|
||||
position: Some(niri_config::Position {
|
||||
x: requested_config.x,
|
||||
y: requested_config.y,
|
||||
}),
|
||||
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!(
|
||||
"Could not parse mode '{}': {}",
|
||||
mode, e
|
||||
))
|
||||
})?),
|
||||
// FIXME: VRR
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if new_conf.is_empty() {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"At least one output must be enabled".to_owned(),
|
||||
));
|
||||
}
|
||||
for output in current_conf.values() {
|
||||
if !new_conf.contains_key(&output.name) {
|
||||
new_conf.insert(output.name.clone(), None);
|
||||
}
|
||||
}
|
||||
if method == 0 {
|
||||
// 0 means "verify", so don't actually apply here
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(err) = self.to_niri.send(new_conf) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn power_save_mode(&self) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
|
||||
Err(zbus::Error::Unsupported)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn panel_orientation_managed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn apply_monitors_config_allowed(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn night_light_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
// FIXME: monitors-changed signal.
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
pub fn new(
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
to_niri,
|
||||
ipc_outputs,
|
||||
}
|
||||
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
|
||||
Self { connectors }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,48 +104,3 @@ impl Start for DisplayConfig {
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from Mutter.
|
||||
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
|
||||
if is_laptop_panel {
|
||||
return String::from("Built-in display");
|
||||
}
|
||||
|
||||
let make = &output.make;
|
||||
let model = &output.model;
|
||||
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
|
||||
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
|
||||
format_diagonal(diagonal)
|
||||
}) {
|
||||
format!("{make} {diagonal}")
|
||||
} else if model != "Unknown" {
|
||||
format!("{make} {model}")
|
||||
} else {
|
||||
make.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_diagonal(diagonal_inches: f64) -> String {
|
||||
let known = [12.1, 13.3, 15.6];
|
||||
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
|
||||
format!("{d:.1}″")
|
||||
} else {
|
||||
format!("{}″", diagonal_inches.round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_diagonal() {
|
||||
assert_snapshot!(format_diagonal(12.11), @"12.1″");
|
||||
assert_snapshot!(format_diagonal(13.28), @"13.3″");
|
||||
assert_snapshot!(format_diagonal(15.6), @"15.6″");
|
||||
assert_snapshot!(format_diagonal(23.2), @"23″");
|
||||
assert_snapshot!(format_diagonal(24.8), @"25″");
|
||||
}
|
||||
}
|
||||
|
||||
+38
-172
@@ -4,30 +4,27 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::{InterfaceRef, SignalEmitter};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||
use zbus::{fdo, interface, ObjectServer};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
|
||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenCast {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
id: usize,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
|
||||
stopped: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
||||
@@ -47,65 +44,27 @@ struct RecordMonitorProperties {
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, DeserializeDict, Type)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct RecordWindowProperties {
|
||||
#[zvariant(rename = "window-id")]
|
||||
window_id: u64,
|
||||
#[zvariant(rename = "cursor-mode")]
|
||||
cursor_mode: Option<CursorMode>,
|
||||
#[zvariant(rename = "is-recording")]
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
output: Output,
|
||||
cursor_mode: CursorMode,
|
||||
was_started: Arc<AtomicBool>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum StreamTarget {
|
||||
// FIXME: update on scale changes and whatnot.
|
||||
Output(niri_ipc::Output),
|
||||
Window { id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StreamTargetId {
|
||||
Output { name: String },
|
||||
Window { id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct StreamParameters {
|
||||
/// Position of the stream in logical coordinates.
|
||||
position: (i32, i32),
|
||||
/// Size of the stream in logical coordinates.
|
||||
size: (i32, i32),
|
||||
}
|
||||
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
target: StreamTargetId,
|
||||
output: Output,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
signal_ctx: SignalContext<'static>,
|
||||
},
|
||||
StopCast {
|
||||
session_id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
impl ScreenCast {
|
||||
async fn create_session(
|
||||
&self,
|
||||
@@ -123,7 +82,7 @@ impl ScreenCast {
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
|
||||
let session = Session::new(session_id, self.connectors.clone(), self.to_niri.clone());
|
||||
match server.at(&path, session.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
@@ -140,34 +99,29 @@ impl ScreenCast {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
#[dbus_interface(property)]
|
||||
async fn version(&self) -> i32 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
impl Session {
|
||||
async fn start(&self) {
|
||||
debug!("start");
|
||||
|
||||
for (stream, iface) in &*self.streams.lock().unwrap() {
|
||||
stream.start(iface.signal_emitter().clone());
|
||||
stream.start(self.id, iface.signal_context().clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(
|
||||
&self,
|
||||
#[zbus(object_server)] server: &ObjectServer,
|
||||
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
|
||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
||||
) {
|
||||
debug!("stop");
|
||||
|
||||
if self.stopped.swap(true, Ordering::SeqCst) {
|
||||
// Already stopped.
|
||||
return;
|
||||
}
|
||||
|
||||
Session::closed(&ctxt).await.unwrap();
|
||||
|
||||
if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast {
|
||||
@@ -179,7 +133,7 @@ impl Session {
|
||||
let streams = mem::take(&mut *self.streams.lock().unwrap());
|
||||
for (_, iface) in streams.iter() {
|
||||
server
|
||||
.remove::<Stream, _>(iface.signal_emitter().path())
|
||||
.remove::<Stream, _>(iface.signal_context().path())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -195,32 +149,20 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(connector, ?properties, "record_monitor");
|
||||
|
||||
let output = {
|
||||
let ipc_outputs = self.ipc_outputs.lock().unwrap();
|
||||
ipc_outputs.values().find(|o| o.name == connector).cloned()
|
||||
};
|
||||
let Some(output) = output else {
|
||||
let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else {
|
||||
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
||||
};
|
||||
|
||||
if output.logical.is_none() {
|
||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||
}
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
||||
let path = format!(
|
||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||
NUMBER.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
|
||||
let target = StreamTarget::Output(output);
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
@@ -237,83 +179,24 @@ impl Session {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn record_window(
|
||||
&mut self,
|
||||
#[zbus(object_server)] server: &ObjectServer,
|
||||
properties: RecordWindowProperties,
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(?properties, "record_window");
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
|
||||
let target = StreamTarget::Window {
|
||||
id: properties.window_id,
|
||||
};
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
self.streams.lock().unwrap().push((stream, iface));
|
||||
}
|
||||
Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())),
|
||||
Err(err) => {
|
||||
return Err(fdo::Error::Failed(format!(
|
||||
"error creating stream object: {err:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
impl Stream {
|
||||
#[zbus(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[zbus(property)]
|
||||
async fn parameters(&self) -> StreamParameters {
|
||||
match &self.target {
|
||||
StreamTarget::Output(output) => {
|
||||
let logical = output.logical.as_ref().unwrap();
|
||||
StreamParameters {
|
||||
position: (logical.x, logical.y),
|
||||
size: (logical.width as i32, logical.height as i32),
|
||||
}
|
||||
}
|
||||
StreamTarget::Window { .. } => {
|
||||
// Does any consumer need this?
|
||||
StreamParameters {
|
||||
position: (0, 0),
|
||||
size: (1, 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCast {
|
||||
pub fn new(
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ipc_outputs,
|
||||
connectors,
|
||||
to_niri,
|
||||
sessions: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
@@ -338,15 +221,14 @@ impl Start for ScreenCast {
|
||||
impl Session {
|
||||
pub fn new(
|
||||
id: usize,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
ipc_outputs,
|
||||
connectors,
|
||||
streams: Arc::new(Mutex::new(vec![])),
|
||||
to_niri,
|
||||
stopped: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,32 +242,27 @@ impl Drop for Session {
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
fn new(
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
pub fn new(
|
||||
output: Output,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
session_id,
|
||||
target,
|
||||
output,
|
||||
cursor_mode,
|
||||
was_started: Arc::new(AtomicBool::new(false)),
|
||||
to_niri,
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, ctxt: SignalEmitter<'static>) {
|
||||
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
|
||||
if self.was_started.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = ScreenCastToNiri::StartCast {
|
||||
session_id: self.session_id,
|
||||
stream_id: self.id,
|
||||
target: self.target.make_id(),
|
||||
session_id,
|
||||
output: self.output.clone(),
|
||||
cursor_mode: self.cursor_mode,
|
||||
signal_ctx: ctxt,
|
||||
};
|
||||
@@ -395,14 +272,3 @@ impl Stream {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTarget {
|
||||
fn make_id(&self) -> StreamTargetId {
|
||||
match self {
|
||||
StreamTarget::Output(output) => StreamTargetId::Output {
|
||||
name: output.name.clone(),
|
||||
},
|
||||
StreamTarget::Window { id } => StreamTargetId::Window { id: *id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use zbus::{fdo, interface, zvariant};
|
||||
use smithay::reexports::wayland_server::DisplayHandle;
|
||||
use zbus::dbus_interface;
|
||||
|
||||
use super::Start;
|
||||
use crate::niri::NewClient;
|
||||
use crate::niri::ClientState;
|
||||
|
||||
pub struct ServiceChannel {
|
||||
to_niri: calloop::channel::Sender<NewClient>,
|
||||
display: DisplayHandle,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
impl ServiceChannel {
|
||||
async fn open_wayland_service_connection(
|
||||
&mut self,
|
||||
service_client_type: u32,
|
||||
) -> fdo::Result<zvariant::OwnedFd> {
|
||||
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
|
||||
if service_client_type != 1 {
|
||||
return Err(fdo::Error::InvalidArgs(
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid service client type".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||
let client = NewClient {
|
||||
client: sock2,
|
||||
restricted: false,
|
||||
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||
credentials_unknown: true,
|
||||
};
|
||||
if let Err(err) = self.to_niri.send(client) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||
self.display
|
||||
.insert_client(sock2, Arc::new(ClientState::default()))
|
||||
.unwrap();
|
||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceChannel {
|
||||
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
|
||||
Self { to_niri }
|
||||
pub fn new(display: DisplayHandle) -> Self {
|
||||
Self { display }
|
||||
}
|
||||
}
|
||||
|
||||
impl Start for ServiceChannel {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let conn = zbus::blocking::connection::Builder::session()?
|
||||
let conn = zbus::blocking::ConnectionBuilder::session()?
|
||||
.name("org.gnome.Mutter.ServiceChannel")?
|
||||
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
|
||||
.build()?;
|
||||
|
||||
+2
-24
@@ -7,11 +7,10 @@ use crate::utils::get_monotonic_time;
|
||||
pub struct FrameClock {
|
||||
last_presentation_time: Option<Duration>,
|
||||
refresh_interval_ns: Option<NonZeroU64>,
|
||||
vrr: bool,
|
||||
}
|
||||
|
||||
impl FrameClock {
|
||||
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
|
||||
pub fn new(refresh_interval: Option<Duration>) -> Self {
|
||||
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
|
||||
assert_eq!(interval.as_secs(), 0);
|
||||
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
|
||||
@@ -22,7 +21,6 @@ impl FrameClock {
|
||||
Self {
|
||||
last_presentation_time: None,
|
||||
refresh_interval_ns,
|
||||
vrr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +29,6 @@ impl FrameClock {
|
||||
.map(|r| Duration::from_nanos(r.get()))
|
||||
}
|
||||
|
||||
pub fn set_vrr(&mut self, vrr: bool) {
|
||||
if self.vrr == vrr {
|
||||
return;
|
||||
}
|
||||
|
||||
self.vrr = vrr;
|
||||
self.last_presentation_time = None;
|
||||
}
|
||||
|
||||
pub fn vrr(&self) -> bool {
|
||||
self.vrr
|
||||
}
|
||||
|
||||
pub fn presented(&mut self, presentation_time: Duration) {
|
||||
if presentation_time.is_zero() {
|
||||
// Not interested in these.
|
||||
@@ -86,13 +71,6 @@ impl FrameClock {
|
||||
let since_last_ns =
|
||||
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
|
||||
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
|
||||
|
||||
// If VRR is enabled and more than one frame passed since last presentation, assume that we
|
||||
// can present immediately.
|
||||
if self.vrr && to_next_ns > refresh_interval_ns {
|
||||
now
|
||||
} else {
|
||||
last_presentation_time + Duration::from_nanos(to_next_ns)
|
||||
}
|
||||
last_presentation_time + Duration::from_nanos(to_next_ns)
|
||||
}
|
||||
}
|
||||
|
||||
+75
-384
@@ -1,30 +1,24 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use niri_ipc::PositionChange;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{Client, Resource};
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, remove_pre_commit_hook,
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, send_surface_state,
|
||||
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
|
||||
SurfaceAttributes,
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{delegate_compositor, delegate_shm};
|
||||
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||
use crate::niri::{CastTarget, ClientState, LockState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
use super::xdg_shell;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::clone2;
|
||||
|
||||
impl CompositorHandler for State {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
@@ -42,21 +36,50 @@ impl CompositorHandler for State {
|
||||
}
|
||||
|
||||
if let Some(output) = self.niri.output_for_root(&root) {
|
||||
let scale = output.current_scale();
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
send_surface_state(surface, data, scale, transform);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: &WlSurface) {
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.pending::<SurfaceAttributes>()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
let client = surface.client().unwrap();
|
||||
let res = state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
let _span = tracy_client::span!("CompositorHandler::commit");
|
||||
trace!(surface = ?surface.id(), "commit");
|
||||
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend.early_import(surface);
|
||||
@@ -70,250 +93,55 @@ impl CompositorHandler for State {
|
||||
root_surface = parent;
|
||||
}
|
||||
|
||||
// Update the cached root surface.
|
||||
self.niri
|
||||
.root_surface
|
||||
.insert(surface.clone(), root_surface.clone());
|
||||
|
||||
if surface == &root_surface {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
if is_mapped(surface) {
|
||||
// The toplevel got mapped.
|
||||
let Unmapped {
|
||||
window,
|
||||
state,
|
||||
activation_token_data,
|
||||
} = entry.remove();
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
|
||||
if is_mapped {
|
||||
// The toplevel got mapped.
|
||||
let window = entry.remove();
|
||||
window.on_commit();
|
||||
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
|
||||
let (rules, width, height, is_full_width, output, workspace_id) =
|
||||
if let InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
height,
|
||||
floating_width: _,
|
||||
floating_height: _,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
} = state
|
||||
{
|
||||
// Check that the output is still connected.
|
||||
let output =
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
// Check that the workspace still exists.
|
||||
let workspace_id = workspace_name
|
||||
.as_deref()
|
||||
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
|
||||
.map(|(_, ws)| ws.id());
|
||||
|
||||
(rules, width, height, is_full_width, output, workspace_id)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(ResolvedWindowRules::empty(), None, None, false, None, None)
|
||||
};
|
||||
|
||||
// The GTK about dialog sets min/max size after the initial configure but
|
||||
// before mapping, so we need to compute open_floating at the last possible
|
||||
// moment, that is here.
|
||||
let is_floating = rules.compute_open_floating(toplevel);
|
||||
|
||||
// Figure out if we should activate the window.
|
||||
let activate = rules.open_focused.map(|focus| {
|
||||
if focus {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
ActivateWindow::No
|
||||
}
|
||||
});
|
||||
let activate = activate.unwrap_or_else(|| {
|
||||
// Check the token timestamp again in case the window took a while between
|
||||
// requesting activation and mapping.
|
||||
let token = activation_token_data.filter(|token| {
|
||||
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
|
||||
});
|
||||
if token.is_some() {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.strict_new_window_focus_policy {
|
||||
ActivateWindow::No
|
||||
} else {
|
||||
ActivateWindow::Smart
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let parent = toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
// 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)| {
|
||||
parent_output.is_none()
|
||||
|| output.is_none()
|
||||
|| output.as_ref() == *parent_output
|
||||
})
|
||||
.map(|(mapped, _)| mapped.window.clone());
|
||||
|
||||
// The mapped pre-commit hook deals with dma-bufs on its own.
|
||||
self.remove_default_dmabuf_pre_commit_hook(toplevel.wl_surface());
|
||||
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let window = mapped.window.clone();
|
||||
|
||||
let target = if let Some(p) = &parent {
|
||||
// Open dialogs next to their parent window.
|
||||
AddWindowTarget::NextTo(p)
|
||||
} else if let Some(id) = workspace_id {
|
||||
AddWindowTarget::Workspace(id)
|
||||
} else if let Some(output) = &output {
|
||||
AddWindowTarget::Output(output)
|
||||
} else {
|
||||
AddWindowTarget::Auto
|
||||
};
|
||||
let output = self.niri.layout.add_window(
|
||||
mapped,
|
||||
target,
|
||||
width,
|
||||
height,
|
||||
is_full_width,
|
||||
is_floating,
|
||||
activate,
|
||||
);
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
self.niri.layout.start_open_animation_for_window(&window);
|
||||
|
||||
let new_focus = self.niri.layout.focus().map(|m| &m.window);
|
||||
if new_focus == Some(&window) {
|
||||
// We activated the newly opened window.
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
|
||||
{
|
||||
self.niri.queue_redraw(output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The toplevel remains unmapped.
|
||||
let unmapped = entry.get();
|
||||
if unmapped.needs_initial_configure() {
|
||||
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
|
||||
self.queue_initial_configure(toplevel);
|
||||
}
|
||||
let window = entry.get();
|
||||
xdg_shell::send_initial_configure_if_needed(window.toplevel());
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.cloned();
|
||||
|
||||
let id = mapped.id();
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped = is_mapped(surface);
|
||||
|
||||
// Must start the close animation before window.on_commit().
|
||||
let transaction = Transaction::new();
|
||||
if !is_mapped {
|
||||
let blocker = transaction.blocker();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri
|
||||
.layout
|
||||
.start_close_animation_for_window(renderer, &window, blocker);
|
||||
});
|
||||
}
|
||||
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
|
||||
let (window, output) = clone2(win_out);
|
||||
|
||||
window.on_commit();
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
|
||||
if !is_mapped {
|
||||
// The toplevel got unmapped.
|
||||
//
|
||||
// Test client: wleird-unmap.
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
self.niri
|
||||
.stop_casts_for_target(CastTarget::Window { id: id.get() });
|
||||
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
|
||||
// If this is the only instance, then this transaction will complete
|
||||
// immediately, so no need to set the timer.
|
||||
if !transaction.is_last() {
|
||||
transaction.register_deadline_timer(&self.niri.event_loop);
|
||||
}
|
||||
|
||||
if was_active {
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
// Newly-unmapped toplevels must perform the initial commit-configure sequence
|
||||
// afresh.
|
||||
let unmapped = Unmapped::new(window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
self.niri.layout.remove_window(&window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), window);
|
||||
self.niri.queue_redraw(output);
|
||||
return;
|
||||
}
|
||||
|
||||
let (serial, buffer_delta) = with_states(surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
(role.configure_serial, buffer_delta)
|
||||
});
|
||||
if serial.is_none() {
|
||||
error!("commit on a mapped surface without a configured serial");
|
||||
}
|
||||
|
||||
// The toplevel remains mapped.
|
||||
self.niri.layout.update_window(&window, serial);
|
||||
|
||||
// Move the toplevel according to the attach offset.
|
||||
if let Some(delta) = buffer_delta {
|
||||
if delta.x != 0 || delta.y != 0 {
|
||||
let (x, y) = delta.to_f64().into();
|
||||
self.niri.layout.move_floating_window(
|
||||
Some(&window),
|
||||
PositionChange::AdjustFixed(x),
|
||||
PositionChange::AdjustFixed(y),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.niri.layout.update_window(&window);
|
||||
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window);
|
||||
self.update_reactive_popups(&window, &output);
|
||||
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
self.niri.queue_redraw(output);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,14 +150,10 @@ impl CompositorHandler for State {
|
||||
|
||||
// This is a commit of a non-root or a non-toplevel root.
|
||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||
if let Some((mapped, output)) = root_window_output {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.cloned();
|
||||
if let Some((window, output)) = root_window_output.map(clone2) {
|
||||
window.on_commit();
|
||||
self.niri.layout.update_window(&window, None);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
self.niri.layout.update_window(&window);
|
||||
self.niri.queue_redraw(output);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -337,116 +161,37 @@ impl CompositorHandler for State {
|
||||
self.popups_handle_commit(surface);
|
||||
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
||||
if let Some(output) = self.output_for_popup(&popup) {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
self.niri.queue_redraw(output.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a layer-shell surface.
|
||||
if self.layer_shell_handle_commit(surface) {
|
||||
return;
|
||||
}
|
||||
self.layer_shell_handle_commit(surface);
|
||||
|
||||
// This might be a cursor surface.
|
||||
if matches!(
|
||||
&self.niri.cursor_manager.cursor_image(),
|
||||
CursorImageStatus::Surface(s) if s == &root_surface
|
||||
) {
|
||||
// In case the cursor surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the cursor image hotspot
|
||||
if surface == &root_surface {
|
||||
with_states(surface, |states| {
|
||||
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
|
||||
|
||||
if let Some(mut cursor_image_attributes) =
|
||||
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
|
||||
{
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
if let Some(buffer_delta) = buffer_delta {
|
||||
cursor_image_attributes.hotspot -= buffer_delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
|
||||
{
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a DnD icon surface.
|
||||
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
|
||||
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
|
||||
|
||||
// In case the dnd surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the dnd icon offset
|
||||
if surface == &dnd_icon.surface {
|
||||
with_states(&dnd_icon.surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
dnd_icon.offset += buffer_delta;
|
||||
});
|
||||
}
|
||||
|
||||
if self.niri.dnd_icon.as_ref() == Some(surface) {
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a lock surface.
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
|
||||
self.niri.maybe_continue_to_locking();
|
||||
} else {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
if self.niri.is_locked() {
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == surface {
|
||||
self.niri.queue_redraw(output.clone());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This message can trigger for lock surfaces that had a commit right after we unlocked
|
||||
// the session, but that's ok, we don't need to handle them.
|
||||
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
|
||||
}
|
||||
|
||||
fn destroyed(&mut self, surface: &WlSurface) {
|
||||
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
|
||||
// when that happens, so that the closing animation includes all these subsurfaces.
|
||||
//
|
||||
// Test client: alacritty with CSD <= 0.13 (it was fixed in winit afterwards:
|
||||
// https://github.com/rust-windowing/winit/pull/3625).
|
||||
//
|
||||
// This is still not perfect, as this function is called already after the (first)
|
||||
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
|
||||
// gets most of the job done.
|
||||
if let Some(root) = self.niri.root_surface.get(surface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||
let window = mapped.window.clone();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.niri
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,57 +207,3 @@ impl ShmHandler for State {
|
||||
|
||||
delegate_compositor!(State);
|
||||
delegate_shm!(State);
|
||||
|
||||
impl State {
|
||||
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.pending()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res =
|
||||
state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
trace!("added default dmabuf blocker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-114
@@ -1,18 +1,16 @@
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||
use crate::niri::State;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
|
||||
impl WlrLayerShellHandler for State {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
@@ -26,30 +24,17 @@ impl WlrLayerShellHandler for State {
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
let Some(output) = output else {
|
||||
warn!("no output for new layer surface, closing");
|
||||
surface.send_close();
|
||||
return;
|
||||
};
|
||||
|
||||
let wl_surface = surface.wl_surface().clone();
|
||||
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
|
||||
assert!(is_new);
|
||||
|
||||
let output = wl_output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
.or_else(|| self.niri.layout.active_output().cloned())
|
||||
.unwrap();
|
||||
let mut map = layer_map_for_output(&output);
|
||||
map.map_layer(&LayerSurface::new(surface, namespace))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
|
||||
let wl_surface = surface.wl_surface();
|
||||
self.niri.unmapped_layer_surfaces.remove(wl_surface);
|
||||
|
||||
let output = if let Some((output, mut map, layer)) =
|
||||
self.niri.layout.outputs().find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
@@ -60,128 +45,68 @@ impl WlrLayerShellHandler for State {
|
||||
layer.map(|layer| (o.clone(), map, layer))
|
||||
}) {
|
||||
map.unmap_layer(&layer);
|
||||
self.niri.mapped_layer_surfaces.remove(&layer);
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(output) = output {
|
||||
self.niri.output_resized(&output);
|
||||
self.niri.output_resized(output);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
|
||||
self.unconstrain_popup(&PopupKind::Xdg(popup));
|
||||
self.unconstrain_popup(&popup);
|
||||
}
|
||||
}
|
||||
delegate_layer_shell!(State);
|
||||
|
||||
impl State {
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
|
||||
let mut root_surface = surface.clone();
|
||||
while let Some(parent) = get_parent(&root_surface) {
|
||||
root_surface = parent;
|
||||
}
|
||||
|
||||
let output = self
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
|
||||
let Some(output) = self
|
||||
.niri
|
||||
.layout
|
||||
.outputs()
|
||||
.find(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
|
||||
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.is_some()
|
||||
})
|
||||
.cloned();
|
||||
let Some(output) = output else {
|
||||
return false;
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if surface == &root_surface {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
|
||||
let mut map = layer_map_for_output(&output);
|
||||
|
||||
// Arrange the layers before sending the initial configure to respect any size the
|
||||
// client may have sent.
|
||||
map.arrange();
|
||||
let mut map = layer_map_for_output(&output);
|
||||
|
||||
// arrange the layers before sending the initial configure
|
||||
// to respect any size the client may have sent
|
||||
map.arrange();
|
||||
// send the initial configure if relevant
|
||||
if !initial_configure_sent {
|
||||
let layer = map
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.unwrap();
|
||||
|
||||
if initial_configure_sent {
|
||||
if is_mapped(surface) {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_surface_state(surface, data, scale, transform);
|
||||
});
|
||||
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let config = self.niri.config.borrow();
|
||||
let rules = &config.layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules, &config);
|
||||
let prev = self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
.insert(layer.clone(), mapped);
|
||||
if prev.is_some() {
|
||||
error!("MappedLayer was present for an unmapped surface");
|
||||
}
|
||||
}
|
||||
|
||||
// Give focus to newly mapped on-demand surfaces. Some launchers like
|
||||
// lxqt-runner rely on this behavior. While this behavior doesn't make much
|
||||
// sense for other clients like panels, the consensus seems to be that it's not
|
||||
// a big deal since panels generally only open once at the start of the
|
||||
// session.
|
||||
//
|
||||
// Note that:
|
||||
// 1) Exclusive layer surfaces already get focus automatically in
|
||||
// update_keyboard_focus().
|
||||
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand
|
||||
// surfaces in update_keyboard_focus(), so we don't need to check for that
|
||||
// here.
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/641
|
||||
let on_demand = layer.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::OnDemand;
|
||||
if was_unmapped && on_demand {
|
||||
// I guess it'd make sense to check that no higher-layer on-demand surface
|
||||
// has focus, but Smithay's Layer doesn't implement Ord so this would be a
|
||||
// little annoying.
|
||||
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
||||
}
|
||||
} else {
|
||||
self.niri.mapped_layer_surfaces.remove(layer);
|
||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||
}
|
||||
} else {
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
});
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
}
|
||||
drop(map);
|
||||
|
||||
// This will call queue_redraw() inside.
|
||||
self.niri.output_resized(&output);
|
||||
} else {
|
||||
// This is an unsync layer-shell subsurface.
|
||||
self.niri.queue_redraw(&output);
|
||||
layer.layer_surface().send_configure();
|
||||
}
|
||||
drop(map);
|
||||
|
||||
true
|
||||
self.niri.output_resized(output);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-580
@@ -7,101 +7,47 @@ use std::io::Write;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||
};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
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::fractional_scale::FractionalScaleHandler;
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||
ServerDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::ext_data_control::{
|
||||
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::primary_selection::{
|
||||
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
|
||||
};
|
||||
use smithay::wayland::selection::wlr_data_control::{
|
||||
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
|
||||
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
||||
use smithay::wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
||||
};
|
||||
use smithay::wayland::tablet_manager::TabletSeatHandler;
|
||||
use smithay::wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
};
|
||||
use smithay::{
|
||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
|
||||
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
|
||||
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_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
delegate_relative_pointer, delegate_seat, delegate_session_lock, delegate_tablet_manager,
|
||||
delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
use crate::layout::ActivateWindow;
|
||||
use crate::niri::{DndIcon, NewClient, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::protocols::virtual_pointer::{
|
||||
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
|
||||
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
|
||||
VirtualPointerMotionEvent,
|
||||
};
|
||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||
use crate::{
|
||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
use crate::niri::State;
|
||||
use crate::utils::output_size;
|
||||
|
||||
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
|
||||
@@ -124,162 +70,54 @@ 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);
|
||||
delegate_tablet_manager!(State);
|
||||
delegate_pointer_gestures!(State);
|
||||
delegate_relative_pointer!(State);
|
||||
delegate_text_input_manager!(State);
|
||||
|
||||
impl TabletSeatHandler for State {
|
||||
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) {
|
||||
// FIXME: tablet tools should have their own cursors.
|
||||
self.niri.cursor_manager.set_cursor_image(image);
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
delegate_tablet_manager!(State);
|
||||
|
||||
impl PointerConstraintsHandler for State {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
|
||||
// Pointer constraints track pointer focus internally, so make sure it's up to date before
|
||||
// activating a new one.
|
||||
self.refresh_pointer_contents();
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
}
|
||||
|
||||
fn cursor_position_hint(
|
||||
&mut self,
|
||||
surface: &WlSurface,
|
||||
pointer: &PointerHandle<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
||||
constraint.is_some_and(|c| c.is_active())
|
||||
});
|
||||
|
||||
if !is_constraint_active {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
|
||||
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
|
||||
// will change, even though the real pointer focus remains on the Blender surface due to
|
||||
// the click grab.
|
||||
//
|
||||
// Ideally we would just use the constraint surface, but we need its origin. So this is
|
||||
// more of a hack because pointer contents has the surface origin available.
|
||||
//
|
||||
// FIXME: use the constraint surface somehow, don't use pointer contents.
|
||||
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
|
||||
return;
|
||||
};
|
||||
|
||||
if surface_under_pointer != surface {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
let target = self
|
||||
.niri
|
||||
.output_for_root(&root)
|
||||
.and_then(|output| self.niri.global_space.output_geometry(output))
|
||||
.map_or(origin + location, |mut output_geometry| {
|
||||
// i32 sizes are exclusive, but f64 sizes are inclusive.
|
||||
output_geometry.size -= (1, 1).into();
|
||||
(origin + location).constrain(output_geometry.to_f64())
|
||||
});
|
||||
pointer.set_location(target);
|
||||
|
||||
// Redraw to update the cursor position if it's visible.
|
||||
if !self.niri.pointer_hidden {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
self.niri.maybe_activate_pointer_constraint(
|
||||
pointer.current_location(),
|
||||
&self.niri.pointer_focus,
|
||||
);
|
||||
}
|
||||
}
|
||||
delegate_pointer_constraints!(State);
|
||||
|
||||
impl InputMethodHandler for State {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
let popup = PopupKind::InputMethod(surface);
|
||||
let popup = PopupKind::from(surface.clone());
|
||||
if let Some(output) = self.output_for_popup(&popup) {
|
||||
let scale = output.current_scale();
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
let wl_surface = popup.wl_surface();
|
||||
let wl_surface = surface.wl_surface();
|
||||
with_states(wl_surface, |data| {
|
||||
send_scale_transform(wl_surface, data, scale, transform);
|
||||
send_surface_state(wl_surface, data, scale, transform);
|
||||
});
|
||||
}
|
||||
|
||||
self.unconstrain_popup(&popup);
|
||||
|
||||
if let Err(err) = self.niri.popups.track_popup(popup) {
|
||||
warn!("error tracking ime popup {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn popup_repositioned(&mut self, surface: PopupSurface) {
|
||||
let popup = PopupKind::InputMethod(surface);
|
||||
self.unconstrain_popup(&popup);
|
||||
}
|
||||
|
||||
fn dismiss_popup(&mut self, surface: PopupSurface) {
|
||||
if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) {
|
||||
let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
|
||||
}
|
||||
}
|
||||
|
||||
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
|
||||
self.niri
|
||||
.layout
|
||||
.find_window_and_output(parent)
|
||||
.map(|(mapped, _)| mapped.window.geometry())
|
||||
.map(|(window, _)| window.geometry())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardShortcutsInhibitHandler for State {
|
||||
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
|
||||
&mut self.niri.keyboard_shortcuts_inhibit_state
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
|
||||
inhibitor.activate();
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.insert(inhibitor.wl_surface().clone(), inhibitor);
|
||||
}
|
||||
|
||||
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.remove(&inhibitor.wl_surface().clone());
|
||||
}
|
||||
}
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
@@ -297,10 +135,6 @@ impl SelectionHandler for State {
|
||||
|
||||
let buf = user_data.clone();
|
||||
thread::spawn(move || {
|
||||
// Clear O_NONBLOCK, otherwise File::write_all() will stop halfway.
|
||||
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
|
||||
warn!("error clearing flags on selection target fd: {err:?}");
|
||||
}
|
||||
if let Err(err) = File::from(fd).write_all(&buf) {
|
||||
warn!("error writing selection: {err:?}");
|
||||
}
|
||||
@@ -321,63 +155,12 @@ impl ClientDndGrabHandler for State {
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
) {
|
||||
let offset = if let CursorImageStatus::Surface(ref surface) =
|
||||
self.niri.cursor_manager.cursor_image()
|
||||
{
|
||||
with_states(surface, |states| {
|
||||
let hotspot = states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot;
|
||||
Point::from((-hotspot.x, -hotspot.y))
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
|
||||
self.niri.dnd_icon = icon;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
let mut activate_output = true;
|
||||
if let Some(target) = validated.then_some(target).flatten() {
|
||||
let root = self.niri.find_root_shell_surface(&target);
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
activate_output = false;
|
||||
}
|
||||
}
|
||||
|
||||
if activate_output {
|
||||
// Find the output from cursor coordinates.
|
||||
//
|
||||
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if !self.niri.pointer_hidden {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
self.niri.layout.focus_output(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dropped(&mut self, _seat: Seat<Self>) {
|
||||
self.niri.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -395,27 +178,14 @@ impl PrimarySelectionHandler for State {
|
||||
}
|
||||
delegate_primary_selection!(State);
|
||||
|
||||
impl WlrDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &WlrDataControlState {
|
||||
&self.niri.wlr_data_control_state
|
||||
impl DataControlHandler for State {
|
||||
fn data_control_state(&self) -> &DataControlState {
|
||||
&self.niri.data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_data_control!(State);
|
||||
|
||||
impl ExtDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &ExtDataControlState {
|
||||
&self.niri.ext_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_ext_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);
|
||||
@@ -431,10 +201,13 @@ impl DmabufHandler for State {
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
if self.backend.import_dmabuf(&dmabuf) {
|
||||
let _ = notifier.successful::<State>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
match self.backend.import_dmabuf(&dmabuf) {
|
||||
Ok(_) => {
|
||||
let _ = notifier.successful::<State>();
|
||||
}
|
||||
Err(_) => {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,13 +224,11 @@ impl SessionLockHandler for State {
|
||||
|
||||
fn unlock(&mut self) {
|
||||
self.niri.unlock();
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
self.niri.notify_activity();
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
warn!("no Output matching WlOutput");
|
||||
error!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -472,325 +243,11 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
|
||||
let size = output_size(output);
|
||||
states.size = Some(Size::from((size.w as u32, size.h as u32)));
|
||||
});
|
||||
let scale = output.current_scale();
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
let wl_surface = surface.wl_surface();
|
||||
with_states(wl_surface, |data| {
|
||||
send_scale_transform(wl_surface, data, scale, transform);
|
||||
send_surface_state(wl_surface, data, scale, transform);
|
||||
});
|
||||
surface.send_configure();
|
||||
}
|
||||
|
||||
impl SecurityContextHandler for State {
|
||||
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
|
||||
self.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |client, _, state| {
|
||||
trace!("inserting a new restricted client, context={context:?}");
|
||||
state.niri.insert_client(NewClient {
|
||||
client,
|
||||
restricted: true,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
delegate_security_context!(State);
|
||||
|
||||
impl IdleNotifierHandler for State {
|
||||
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
||||
&mut self.niri.idle_notifier_state
|
||||
}
|
||||
}
|
||||
delegate_idle_notify!(State);
|
||||
|
||||
impl IdleInhibitHandler for State {
|
||||
fn inhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.insert(surface);
|
||||
}
|
||||
|
||||
fn uninhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.remove(&surface);
|
||||
}
|
||||
}
|
||||
delegate_idle_inhibit!(State);
|
||||
|
||||
impl ForeignToplevelHandler for State {
|
||||
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
|
||||
&mut self.niri.foreign_toplevel_state
|
||||
}
|
||||
|
||||
fn activate(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
mapped.toplevel().send_close();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||
{
|
||||
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
|
||||
role.current
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
});
|
||||
if !has_fullscreen_cap {
|
||||
return;
|
||||
}
|
||||
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
&requested_output,
|
||||
None,
|
||||
ActivateWindow::Smart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_foreign_toplevel!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
|
||||
trace!("screencopy manager destroyed already");
|
||||
return;
|
||||
};
|
||||
queue.push(screencopy);
|
||||
} else {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if let Err(err) = self
|
||||
.niri
|
||||
.render_for_screencopy_without_damage(renderer, manager, screencopy)
|
||||
{
|
||||
warn!("error rendering for screencopy: {err:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState {
|
||||
&mut self.niri.screencopy_state
|
||||
}
|
||||
}
|
||||
delegate_screencopy!(State);
|
||||
|
||||
impl VirtualPointerHandler for State {
|
||||
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
|
||||
&mut self.niri.virtual_pointer_state
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
|
||||
self.process_input_event(
|
||||
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
|
||||
);
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
|
||||
}
|
||||
}
|
||||
delegate_virtual_pointer!(State);
|
||||
|
||||
impl DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.drm_lease_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn lease_request(
|
||||
&mut self,
|
||||
node: DrmNode,
|
||||
request: DrmLeaseRequest,
|
||||
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||
debug!(
|
||||
"Received lease request for {} connectors",
|
||||
request.connectors.len()
|
||||
);
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.lease_request(request)
|
||||
}
|
||||
|
||||
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
|
||||
debug!("Lease success");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.new_lease(lease);
|
||||
}
|
||||
|
||||
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
|
||||
debug!("Destroyed lease");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.remove_lease(lease_id);
|
||||
}
|
||||
}
|
||||
delegate_drm_lease!(State);
|
||||
|
||||
delegate_viewporter!(State);
|
||||
|
||||
impl GammaControlHandler for State {
|
||||
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState {
|
||||
&mut self.niri.gamma_control_manager_state
|
||||
}
|
||||
|
||||
fn get_gamma_size(&mut self, output: &Output) -> Option<u32> {
|
||||
match self.backend.tty().get_gamma_size(output) {
|
||||
Ok(0) => None, // Setting gamma is not supported.
|
||||
Ok(size) => Some(size),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"error getting gamma size for output {}: {err:?}",
|
||||
output.name()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()> {
|
||||
match self.backend.tty().set_gamma(output, ramp) {
|
||||
Ok(()) => Some(()),
|
||||
Err(err) => {
|
||||
warn!("error setting gamma for output {}: {err:?}", output.name());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_gamma_control!(State);
|
||||
|
||||
impl XdgActivationHandler for State {
|
||||
fn activation_state(&mut self) -> &mut XdgActivationState {
|
||||
&mut self.niri.activation_state
|
||||
}
|
||||
|
||||
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
||||
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
|
||||
// common client behavior.
|
||||
//
|
||||
// We don't have urgency yet, so just ignore such tokens.
|
||||
//
|
||||
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
|
||||
let Some((serial, seat)) = data.serial else {
|
||||
return false;
|
||||
};
|
||||
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
|
||||
// upon clicking on their tray icon or on their notification. This debug flag makes that
|
||||
// work.
|
||||
//
|
||||
// Clicking on a notification sends clients a perfectly valid activation token from the
|
||||
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
|
||||
// and we can remove this debug flag.
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.honor_xdg_activation_with_invalid_serial {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
|
||||
// with no keyboard interactivity won't have any keyboard focus.
|
||||
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
|
||||
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
|
||||
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
token_data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
|
||||
unmapped.activation_token_data = Some(token_data);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.activation_state.remove_token(&token);
|
||||
}
|
||||
}
|
||||
delegate_xdg_activation!(State);
|
||||
|
||||
impl FractionalScaleHandler for State {}
|
||||
delegate_fractional_scale!(State);
|
||||
|
||||
impl OutputManagementHandler for State {
|
||||
fn output_management_state(&mut self) -> &mut OutputManagementManagerState {
|
||||
&mut self.niri.output_management_state
|
||||
}
|
||||
|
||||
fn apply_output_config(&mut self, config: niri_config::Outputs) {
|
||||
self.niri.config.borrow_mut().outputs = config;
|
||||
self.reload_output_config();
|
||||
}
|
||||
}
|
||||
delegate_output_management!(State);
|
||||
|
||||
impl MutterX11InteropHandler for State {}
|
||||
delegate_mutter_x11_interop!(State);
|
||||
|
||||
delegate_single_pixel_buffer!(State);
|
||||
|
||||
+165
-1082
File diff suppressed because it is too large
Load Diff
+1465
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
use ::input as libinput;
|
||||
use smithay::backend::input;
|
||||
use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
type NiriDevice: NiriInputDevice;
|
||||
}
|
||||
impl<T: input::InputBackend> NiriInputBackend for T
|
||||
where
|
||||
Self::Device: NiriInputDevice,
|
||||
{
|
||||
type NiriDevice = Self::Device;
|
||||
}
|
||||
|
||||
pub trait NiriInputDevice: input::Device {
|
||||
// FIXME: this should maybe be per-event, not per-device,
|
||||
// but it's not clear that this matters in practice?
|
||||
// it might be more obvious once we implement it for libinput
|
||||
fn output(&self, state: &State) -> Option<Output>;
|
||||
}
|
||||
|
||||
impl NiriInputDevice for libinput::Device {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: Allow specifying the output per-device?
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for WinitVirtualDevice {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: we should be returning the single output that the winit backend creates,
|
||||
// but for now, that will cause issues because the output is normally upside down,
|
||||
// so we apply Transform::Flipped180 to it and that would also cause
|
||||
// the cursor position to be flipped, which is not what we want.
|
||||
//
|
||||
// instead, we just return None and rely on the fact that it has only one output.
|
||||
// doing so causes the cursor to be placed in *global* output coordinates,
|
||||
// which are not flipped, and happen to be what we want.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
}
|
||||
}
|
||||
-4386
File diff suppressed because it is too large
Load Diff
@@ -1,240 +0,0 @@
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
|
||||
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
||||
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
Move,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
use_threshold: bool,
|
||||
) -> Self {
|
||||
let gesture = if use_threshold {
|
||||
GestureState::Recognizing
|
||||
} else {
|
||||
GestureState::Move
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for MoveGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
self.gesture = GestureState::Move;
|
||||
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
|
||||
if self.gesture != GestureState::Move {
|
||||
return;
|
||||
}
|
||||
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// When moving with the left button, right toggles floating, and vice versa.
|
||||
let toggle_floating_button = if self.start_data.button == 0x110 {
|
||||
0x111
|
||||
} else {
|
||||
0x110
|
||||
};
|
||||
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
|
||||
data.niri.layout.toggle_window_floating(Some(&self.window));
|
||||
}
|
||||
|
||||
if !handle.current_pressed().contains(&self.start_data.button) {
|
||||
// The button that initiated the grab was released.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use niri_ipc::PickedColor;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::{render_to_vec, RenderTarget};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
}
|
||||
|
||||
impl PickColorGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
if let Some(tx) = state.niri.pick_color.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
|
||||
let (output, pos_within_output) = data.niri.output_under(location)?;
|
||||
let output = output.clone();
|
||||
|
||||
data.backend
|
||||
.with_primary_renderer(|renderer| {
|
||||
data.niri.update_render_elements(Some(&output));
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
|
||||
// at the bottom/right edges of the monitors.
|
||||
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||
let size = Size::<i32, Physical>::from((1, 1));
|
||||
|
||||
let elements = data.niri.render(
|
||||
renderer,
|
||||
&output,
|
||||
false,
|
||||
// This is an interactive operation so we can render without blocking out.
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
let pixels = match render_to_vec(
|
||||
renderer,
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements.iter().rev().map(|elem| {
|
||||
let offset = pos.upscale(-1);
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
}),
|
||||
) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if pixels.len() == 4 {
|
||||
let rgb = [
|
||||
f64::from(pixels[0]) / 255.0,
|
||||
f64::from(pixels[1]) / 255.0,
|
||||
f64::from(pixels[2]) / 255.0,
|
||||
];
|
||||
Some(PickedColor { rgb })
|
||||
} else {
|
||||
error!(
|
||||
"unexpected pixel data length: {} (expected 4)",
|
||||
pixels.len()
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for PickColorGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_color.take() {
|
||||
let color = Self::pick_color_at_point(handle.current_location(), data);
|
||||
let _ = tx.send_blocking(color);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::window::Mapped;
|
||||
|
||||
pub struct PickWindowGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
}
|
||||
|
||||
impl PickWindowGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
if let Some(tx) = state.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for PickWindowGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(
|
||||
data.niri
|
||||
.window_under(handle.current_location())
|
||||
.map(Mapped::id),
|
||||
);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct ResizeGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl ResizeGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for ResizeGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
if self.window.alive() {
|
||||
let delta = event.location - self.start_data.location;
|
||||
let ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
pub struct ScrollTracker {
|
||||
tick: f64,
|
||||
last: f64,
|
||||
acc: f64,
|
||||
}
|
||||
|
||||
impl ScrollTracker {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(tick: i8) -> Self {
|
||||
Self {
|
||||
tick: f64::from(tick),
|
||||
last: 0.,
|
||||
acc: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accumulate(&mut self, amount: f64) -> i8 {
|
||||
let changed_direction = (self.last > 0. && amount < 0.) || (self.last < 0. && amount > 0.);
|
||||
if changed_direction {
|
||||
self.acc = 0.
|
||||
}
|
||||
|
||||
self.last = amount;
|
||||
self.acc += amount;
|
||||
|
||||
let mut ticks = 0;
|
||||
if self.acc.abs() >= self.tick {
|
||||
let clamped = self.acc.clamp(-127. * self.tick, 127. * self.tick);
|
||||
ticks = (clamped as i16 / self.tick as i16) as i8;
|
||||
self.acc %= self.tick;
|
||||
}
|
||||
|
||||
ticks
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.last = 0.;
|
||||
self.acc = 0.;
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
WorkspaceSwitch,
|
||||
}
|
||||
|
||||
impl SpatialMovementGrab {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
GestureState::Recognizing
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
workspace_id,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_end(false, Some(false))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
let layout = &mut data.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
if c.x.abs() > c.y.abs() {
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
layout.workspace_switch_gesture_update(-c.y, timestamp, false)
|
||||
}
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
layout.view_offset_gesture_update(-delta.x, timestamp, false)
|
||||
}
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_update(-delta.y, timestamp, false)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
} else {
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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,136 +0,0 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchMoveGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchMoveGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchMoveGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchResizeGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchResizeGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchResizeGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
let delta = event.location - self.start_data.location;
|
||||
let ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,574 +0,0 @@
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
|
||||
Transform, Window,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::cli::Msg;
|
||||
use crate::utils::version;
|
||||
|
||||
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let request = match &msg {
|
||||
Msg::Version => Request::Version,
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::FocusedWindow => Request::FocusedWindow,
|
||||
Msg::FocusedOutput => Request::FocusedOutput,
|
||||
Msg::PickWindow => Request::PickWindow,
|
||||
Msg::PickColor => Request::PickColor,
|
||||
Msg::Action { action } => Request::Action(action.clone()),
|
||||
Msg::Output { output, action } => Request::Output {
|
||||
output: output.clone(),
|
||||
action: action.clone(),
|
||||
},
|
||||
Msg::Workspaces => Request::Workspaces,
|
||||
Msg::Windows => Request::Windows,
|
||||
Msg::Layers => Request::Layers,
|
||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
};
|
||||
|
||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
|
||||
let (reply, mut read_event) = socket
|
||||
.send(request)
|
||||
.context("error communicating with niri")?;
|
||||
|
||||
let compositor_version = match reply {
|
||||
Err(_) if !matches!(msg, Msg::Version) => {
|
||||
// If we got an error, it might be that the CLI is a different version from the running
|
||||
// niri instance. Request the running instance version to compare and print a message.
|
||||
Socket::connect()
|
||||
.and_then(|socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
.map(|(reply, _read_event)| reply)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||
unsafe {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let response = reply.map_err(|err_msg| {
|
||||
// Check for CLI-server version mismatch to add helpful context.
|
||||
match compositor_version {
|
||||
Some(Ok(Response::Version(compositor_version))) => {
|
||||
let cli_version = version();
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||
eprintln!("Compositor version: {compositor_version}");
|
||||
eprintln!("CLI version: {cli_version}");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!("Unable to get the running niri compositor version.");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
None => {
|
||||
// Communication error, or the original request was already a version request.
|
||||
// Don't add irrelevant context.
|
||||
}
|
||||
}
|
||||
|
||||
anyhow!(err_msg).context("niri returned an error")
|
||||
})?;
|
||||
|
||||
match msg {
|
||||
Msg::RequestError => {
|
||||
bail!("unexpected response: expected an error, got {response:?}");
|
||||
}
|
||||
Msg::Version => {
|
||||
let Response::Version(compositor_version) = response else {
|
||||
bail!("unexpected response: expected Version, got {response:?}");
|
||||
};
|
||||
|
||||
let cli_version = version();
|
||||
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
json!({
|
||||
"compositor": compositor_version,
|
||||
"cli": cli_version,
|
||||
})
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI.");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
println!("Compositor version: {compositor_version}");
|
||||
println!("CLI version: {cli_version}");
|
||||
}
|
||||
Msg::Outputs => {
|
||||
let Response::Outputs(outputs) = response else {
|
||||
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let output =
|
||||
serde_json::to_string(&outputs).context("error formatting response")?;
|
||||
println!("{output}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut outputs = outputs
|
||||
.into_values()
|
||||
.map(|out| (OutputName::from_ipc_output(&out), out))
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_unstable_by(|a, b| a.0.compare(&b.0));
|
||||
|
||||
for (_name, output) in outputs.into_iter() {
|
||||
print_output(output)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::FocusedWindow => {
|
||||
let Response::FocusedWindow(window) = response else {
|
||||
bail!("unexpected response: expected FocusedWindow, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||
println!("{window}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(window) = window {
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window is focused.");
|
||||
}
|
||||
}
|
||||
Msg::Windows => {
|
||||
let Response::Windows(mut windows) = response else {
|
||||
bail!("unexpected response: expected Windows, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let windows =
|
||||
serde_json::to_string(&windows).context("error formatting response")?;
|
||||
println!("{windows}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::Layers => {
|
||||
let Response::Layers(mut layers) = response else {
|
||||
bail!("unexpected response: expected Layers, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let layers = serde_json::to_string(&layers).context("error formatting response")?;
|
||||
println!("{layers}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
layers.sort_by(|a, b| {
|
||||
Ord::cmp(&a.output, &b.output)
|
||||
.then_with(|| Ord::cmp(&a.layer, &b.layer))
|
||||
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
|
||||
});
|
||||
let mut iter = layers.iter().peekable();
|
||||
|
||||
let print = |surface: &niri_ipc::LayerSurface| {
|
||||
println!(" Surface:");
|
||||
println!(" Namespace: \"{}\"", &surface.namespace);
|
||||
|
||||
let interactivity = match surface.keyboard_interactivity {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
|
||||
};
|
||||
println!(" Keyboard interactivity: {interactivity}");
|
||||
};
|
||||
|
||||
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
|
||||
output: &str,
|
||||
layer| {
|
||||
let mut empty = true;
|
||||
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
|
||||
empty = false;
|
||||
println!();
|
||||
print(surface);
|
||||
}
|
||||
if empty {
|
||||
println!(" (empty)\n");
|
||||
} else {
|
||||
println!();
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(surface) = iter.peek() {
|
||||
let output = &surface.output;
|
||||
println!("Output \"{output}\":");
|
||||
|
||||
print!(" Background layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Background);
|
||||
|
||||
print!(" Bottom layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
|
||||
|
||||
print!(" Top layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Top);
|
||||
|
||||
print!(" Overlay layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
|
||||
}
|
||||
}
|
||||
Msg::FocusedOutput => {
|
||||
let Response::FocusedOutput(output) = response else {
|
||||
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let output = serde_json::to_string(&output).context("error formatting response")?;
|
||||
println!("{output}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(output) = output {
|
||||
print_output(output)?;
|
||||
} else {
|
||||
println!("No output is focused.");
|
||||
}
|
||||
}
|
||||
Msg::PickWindow => {
|
||||
let Response::PickedWindow(window) = response else {
|
||||
bail!("unexpected response: expected PickedWindow, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||
println!("{window}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(window) = window {
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window selected.");
|
||||
}
|
||||
}
|
||||
Msg::PickColor => {
|
||||
let Response::PickedColor(color) = response else {
|
||||
bail!("unexpected response: expected PickedColor, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let color = serde_json::to_string(&color).context("error formatting response")?;
|
||||
println!("{color}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(color) = color {
|
||||
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
|
||||
|
||||
println!("Picked color: rgb({r}, {g}, {b})",);
|
||||
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
|
||||
} else {
|
||||
println!("No color was picked.");
|
||||
}
|
||||
}
|
||||
Msg::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
}
|
||||
Msg::Output { output, .. } => {
|
||||
let Response::OutputConfigChanged(response) = response else {
|
||||
bail!("unexpected response: expected OutputConfigChanged, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if response == OutputConfigChanged::OutputWasMissing {
|
||||
println!("Output \"{output}\" is not connected.");
|
||||
println!("The change will apply when it is connected.");
|
||||
}
|
||||
}
|
||||
Msg::Workspaces => {
|
||||
let Response::Workspaces(mut response) = response else {
|
||||
bail!("unexpected response: expected Workspaces, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if response.is_empty() {
|
||||
println!("No workspaces.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
response.sort_by_key(|ws| ws.idx);
|
||||
response.sort_by(|a, b| a.output.cmp(&b.output));
|
||||
|
||||
let mut current_output = if let Some(output) = response[0].output.as_deref() {
|
||||
println!("Output \"{output}\":");
|
||||
Some(output)
|
||||
} else {
|
||||
println!("No output:");
|
||||
None
|
||||
};
|
||||
|
||||
for ws in &response {
|
||||
if ws.output.as_deref() != current_output {
|
||||
let output = ws.output.as_deref().context(
|
||||
"invalid response: workspace with no output \
|
||||
following a workspace with an output",
|
||||
)?;
|
||||
current_output = Some(output);
|
||||
println!("\nOutput \"{output}\":");
|
||||
}
|
||||
|
||||
let is_active = if ws.is_active { " * " } else { " " };
|
||||
let idx = ws.idx;
|
||||
let name = if let Some(name) = ws.name.as_deref() {
|
||||
format!(" \"{name}\"")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!("{is_active}{idx}{name}");
|
||||
}
|
||||
}
|
||||
Msg::KeyboardLayouts => {
|
||||
let Response::KeyboardLayouts(response) = response else {
|
||||
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let KeyboardLayouts { names, current_idx } = response;
|
||||
let current_idx = usize::from(current_idx);
|
||||
|
||||
println!("Keyboard layouts:");
|
||||
for (idx, name) in names.iter().enumerate() {
|
||||
let is_active = if idx == current_idx { " * " } else { " " };
|
||||
println!("{is_active}{idx} {name}");
|
||||
}
|
||||
}
|
||||
Msg::EventStream => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
|
||||
if !json {
|
||||
println!("Started reading events.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let event = read_event().context("error reading event from niri")?;
|
||||
|
||||
if json {
|
||||
let event = serde_json::to_string(&event).context("error formatting event")?;
|
||||
println!("{event}");
|
||||
continue;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
println!("Workspaces changed: {workspaces:?}");
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let word = if focused { "focused" } else { "activated" };
|
||||
println!("Workspace {word}: {id}");
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
println!(
|
||||
"Workspace {workspace_id}: \
|
||||
active window changed to {active_window_id:?}"
|
||||
);
|
||||
}
|
||||
Event::WindowsChanged { windows } => {
|
||||
println!("Windows changed: {windows:?}");
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
println!("Window opened or changed: {window:?}");
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
println!("Window closed: {id}");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
println!("Window focus changed: {id:?}");
|
||||
}
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
println!("Keyboard layout switched: {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
let Output {
|
||||
name,
|
||||
make,
|
||||
model,
|
||||
serial,
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode,
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
} = output;
|
||||
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
println!(r#"Output "{make} {model} {serial}" ({name})"#);
|
||||
|
||||
if let Some(current) = current_mode {
|
||||
let mode = *modes
|
||||
.get(current)
|
||||
.context("invalid response: current mode does not exist")?;
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let preferred = if is_preferred { " (preferred)" } else { "" };
|
||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
|
||||
} else {
|
||||
println!(" Disabled");
|
||||
}
|
||||
|
||||
if vrr_supported {
|
||||
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
|
||||
println!(" Variable refresh rate: supported, {enabled}");
|
||||
} else {
|
||||
println!(" Variable refresh rate: not supported");
|
||||
}
|
||||
|
||||
if let Some((width, height)) = physical_size {
|
||||
println!(" Physical size: {width}x{height} mm");
|
||||
} else {
|
||||
println!(" Physical size: unknown");
|
||||
}
|
||||
|
||||
if let Some(logical) = logical {
|
||||
let LogicalOutput {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
transform,
|
||||
} = logical;
|
||||
println!(" Logical position: {x}, {y}");
|
||||
println!(" Logical size: {width}x{height}");
|
||||
println!(" Scale: {scale}");
|
||||
|
||||
let transform = match transform {
|
||||
Transform::Normal => "normal",
|
||||
Transform::_90 => "90° counter-clockwise",
|
||||
Transform::_180 => "180°",
|
||||
Transform::_270 => "270° counter-clockwise",
|
||||
Transform::Flipped => "flipped horizontally",
|
||||
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
|
||||
Transform::Flipped180 => "flipped vertically",
|
||||
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
|
||||
};
|
||||
println!(" Transform: {transform}");
|
||||
}
|
||||
|
||||
println!(" Available modes:");
|
||||
for (idx, mode) in modes.into_iter().enumerate() {
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
|
||||
let is_current = Some(idx) == current_mode;
|
||||
let qualifier = match (is_current, is_preferred) {
|
||||
(true, true) => " (current, preferred)",
|
||||
(true, false) => " (current)",
|
||||
(false, true) => " (preferred)",
|
||||
(false, false) => "",
|
||||
};
|
||||
|
||||
println!(" {width}x{height}@{refresh:.3}{qualifier}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_window(window: &Window) {
|
||||
let focused = if window.is_focused { " (focused)" } else { "" };
|
||||
println!("Window ID {}:{focused}", window.id);
|
||||
|
||||
if let Some(title) = &window.title {
|
||||
println!(" Title: \"{title}\"");
|
||||
} else {
|
||||
println!(" Title: (unset)");
|
||||
}
|
||||
|
||||
if let Some(app_id) = &window.app_id {
|
||||
println!(" App ID: \"{app_id}\"");
|
||||
} else {
|
||||
println!(" App ID: (unset)");
|
||||
}
|
||||
|
||||
println!(
|
||||
" Is floating: {}",
|
||||
if window.is_floating { "yes" } else { "no" }
|
||||
);
|
||||
|
||||
if let Some(pid) = window.pid {
|
||||
println!(" PID: {pid}");
|
||||
} else {
|
||||
println!(" PID: (unknown)");
|
||||
}
|
||||
|
||||
if let Some(workspace_id) = window.workspace_id {
|
||||
println!(" Workspace ID: {workspace_id}");
|
||||
} else {
|
||||
println!(" Workspace ID: (none)");
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
@@ -1,693 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{env, io, process};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_channel::{Receiver, Sender, TrySendError};
|
||||
use calloop::futures::Scheduler;
|
||||
use calloop::io::Async;
|
||||
use directories::BaseDirs;
|
||||
use futures_util::io::{AsyncReadExt, BufReader};
|
||||
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
|
||||
};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::rustix::fs::unlink;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::input::pick_window_grab::PickWindowGrab;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::{version, with_toplevel_role};
|
||||
use crate::window::Mapped;
|
||||
|
||||
// If an event stream client fails to read events fast enough that we accumulate more than this
|
||||
// number in our buffer, we drop that event stream client.
|
||||
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
||||
|
||||
pub struct IpcServer {
|
||||
/// Path to the IPC socket.
|
||||
///
|
||||
/// This is `None` when creating `IpcServer` without a socket.
|
||||
pub socket_path: Option<PathBuf>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct ClientCtx {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
scheduler: Scheduler<()>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct EventStreamClient {
|
||||
events: Receiver<Event>,
|
||||
disconnect: Receiver<()>,
|
||||
write: Box<dyn AsyncWrite + Unpin>,
|
||||
}
|
||||
|
||||
struct EventStreamSender {
|
||||
events: Sender<Event>,
|
||||
disconnect: Sender<()>,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
pub fn start(
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
wayland_socket_name: Option<&OsStr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("Ipc::start");
|
||||
|
||||
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
|
||||
let wayland_socket_name = wayland_socket_name.to_string_lossy();
|
||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||
let mut socket_path = socket_dir();
|
||||
socket_path.push(socket_name);
|
||||
|
||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("error setting socket to non-blocking")?;
|
||||
|
||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(source, |_, socket, state| {
|
||||
match socket.accept() {
|
||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Some(socket_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
socket_path,
|
||||
event_streams: Rc::new(RefCell::new(Vec::new())),
|
||||
event_stream_state: Rc::new(RefCell::new(EventStreamState::default())),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_event(&self, event: Event) {
|
||||
let mut streams = self.event_streams.borrow_mut();
|
||||
let mut to_remove = Vec::new();
|
||||
for (idx, stream) in streams.iter_mut().enumerate() {
|
||||
match stream.events.try_send(event.clone()) {
|
||||
Ok(()) => (),
|
||||
Err(TrySendError::Closed(_)) => to_remove.push(idx),
|
||||
Err(TrySendError::Full(_)) => {
|
||||
warn!(
|
||||
"disconnecting IPC event stream client \
|
||||
because it is reading events too slowly"
|
||||
);
|
||||
to_remove.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in to_remove.into_iter().rev() {
|
||||
let stream = streams.swap_remove(idx);
|
||||
let _ = stream.disconnect.send_blocking(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IpcServer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(socket_path) = &self.socket_path {
|
||||
let _ = unlink(socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_dir() -> PathBuf {
|
||||
BaseDirs::new()
|
||||
.as_ref()
|
||||
.and_then(|x| x.runtime_dir())
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(env::temp_dir)
|
||||
}
|
||||
|
||||
fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
let _span = tracy_client::span!("on_new_ipc_client");
|
||||
trace!("new IPC client connected");
|
||||
|
||||
let stream = match state.niri.event_loop.adapt_io(stream) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
warn!("error making IPC stream async: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let ipc_server = state.niri.ipc_server.as_ref().unwrap();
|
||||
|
||||
let ctx = ClientCtx {
|
||||
event_loop: state.niri.event_loop.clone(),
|
||||
scheduler: state.niri.scheduler.clone(),
|
||||
ipc_outputs: state.backend.ipc_outputs(),
|
||||
event_streams: ipc_server.event_streams.clone(),
|
||||
event_stream_state: ipc_server.event_stream_state.clone(),
|
||||
};
|
||||
|
||||
let future = async move {
|
||||
if let Err(err) = handle_client(ctx, stream).await {
|
||||
warn!("error handling IPC client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = state.niri.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC stream future: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
||||
let (read, mut write) = stream.split();
|
||||
let mut buf = String::new();
|
||||
|
||||
// Read a single line to allow extensibility in the future to keep reading.
|
||||
BufReader::new(read)
|
||||
.read_line(&mut buf)
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
|
||||
let request = serde_json::from_str(&buf)
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
if let Err(err) = &reply {
|
||||
if !requested_error {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
buf.push(b'\n');
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
if requested_event_stream {
|
||||
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||
|
||||
// Spawn a task for the client.
|
||||
let client = EventStreamClient {
|
||||
events: events_rx,
|
||||
disconnect: disconnect_rx,
|
||||
write: Box::new(write) as _,
|
||||
};
|
||||
let future = async move {
|
||||
if let Err(err) = handle_event_stream_client(client).await {
|
||||
warn!("error handling IPC event stream client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC event stream future: {err:?}");
|
||||
}
|
||||
|
||||
// Send the initial state.
|
||||
{
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
for event in state.replicate() {
|
||||
events_tx
|
||||
.try_send(event)
|
||||
.expect("initial event burst had more events than buffer size");
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the list.
|
||||
{
|
||||
let mut streams = ctx.event_streams.borrow_mut();
|
||||
let sender = EventStreamSender {
|
||||
events: events_tx,
|
||||
disconnect: disconnect_tx,
|
||||
};
|
||||
streams.push(sender);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let response = match request {
|
||||
Request::ReturnError => return Err(String::from("example compositor error")),
|
||||
Request::Version => Response::Version(version()),
|
||||
Request::Outputs => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
|
||||
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
|
||||
Response::Outputs(outputs.collect())
|
||||
}
|
||||
Request::Workspaces => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let workspaces = state.workspaces.workspaces.values().cloned().collect();
|
||||
Response::Workspaces(workspaces)
|
||||
}
|
||||
Request::Windows => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = state.windows.windows.values().cloned().collect();
|
||||
Response::Windows(windows)
|
||||
}
|
||||
Request::Layers => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let mut layers = Vec::new();
|
||||
for output in state.niri.global_space.outputs() {
|
||||
let name = output.name();
|
||||
for surface in layer_map_for_output(output).layers() {
|
||||
let layer = match surface.layer() {
|
||||
Layer::Background => niri_ipc::Layer::Background,
|
||||
Layer::Bottom => niri_ipc::Layer::Bottom,
|
||||
Layer::Top => niri_ipc::Layer::Top,
|
||||
Layer::Overlay => niri_ipc::Layer::Overlay,
|
||||
};
|
||||
let keyboard_interactivity =
|
||||
match surface.cached_state().keyboard_interactivity {
|
||||
KeyboardInteractivity::None => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None
|
||||
}
|
||||
KeyboardInteractivity::Exclusive => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
|
||||
}
|
||||
KeyboardInteractivity::OnDemand => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
|
||||
}
|
||||
};
|
||||
|
||||
layers.push(niri_ipc::LayerSurface {
|
||||
namespace: surface.namespace().to_owned(),
|
||||
output: name.clone(),
|
||||
layer,
|
||||
keyboard_interactivity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send_blocking(layers);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let layers = result.map_err(|_| String::from("error getting layers info"))?;
|
||||
Response::Layers(layers)
|
||||
}
|
||||
Request::KeyboardLayouts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
||||
let layout = layout.expect("keyboard layouts should be set at startup");
|
||||
Response::KeyboardLayouts(layout)
|
||||
}
|
||||
Request::FocusedWindow => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = &state.windows.windows;
|
||||
let window = windows.values().find(|win| win.is_focused).cloned();
|
||||
Response::FocusedWindow(window)
|
||||
}
|
||||
Request::PickWindow => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let pointer = state.niri.seat.get_pointer().unwrap();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: 0,
|
||||
location: pointer.current_location(),
|
||||
};
|
||||
let grab = PickWindowGrab::new(start_data);
|
||||
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
|
||||
// any.
|
||||
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||
state.niri.pick_window = Some(tx);
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let id = result.map_err(|_| String::from("error getting picked window info"))?;
|
||||
let window = id.and_then(|id| {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
state.windows.windows.get(&id.get()).cloned()
|
||||
});
|
||||
Response::PickedWindow(window)
|
||||
}
|
||||
Request::PickColor => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.handle_pick_color(tx);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let color = result.map_err(|_| String::from("error getting picked color"))?;
|
||||
Response::PickedColor(color)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
// Make sure some logic like workspace clean-up has a chance to run before doing
|
||||
// actions.
|
||||
state.niri.advance_animations();
|
||||
state.do_action(action, false);
|
||||
let _ = tx.send_blocking(());
|
||||
});
|
||||
|
||||
// Wait until the action has been processed before returning. This is important for a
|
||||
// few actions, for instance for DoScreenTransition this wait ensures that the screen
|
||||
// contents were sampled into the texture.
|
||||
let _ = rx.recv().await;
|
||||
Response::Handled
|
||||
}
|
||||
Request::Output { output, action } => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
|
||||
let found = ipc_outputs
|
||||
.values()
|
||||
.any(|o| OutputName::from_ipc_output(o).matches(&output));
|
||||
let response = if found {
|
||||
OutputConfigChanged::Applied
|
||||
} else {
|
||||
OutputConfigChanged::OutputWasMissing
|
||||
};
|
||||
drop(ipc_outputs);
|
||||
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.apply_transient_output_config(&output, action);
|
||||
});
|
||||
|
||||
Response::OutputConfigChanged(response)
|
||||
}
|
||||
Request::FocusedOutput => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let active_output = state
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.map(|output| output.name());
|
||||
|
||||
let output = active_output.and_then(|active_output| {
|
||||
state
|
||||
.backend
|
||||
.ipc_outputs()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|o| o.name == active_output)
|
||||
.cloned()
|
||||
});
|
||||
|
||||
let _ = tx.send_blocking(output);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let output = result.map_err(|_| String::from("error getting active output info"))?;
|
||||
Response::FocusedOutput(output)
|
||||
}
|
||||
Request::EventStream => Response::Handled,
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> {
|
||||
let EventStreamClient {
|
||||
events,
|
||||
disconnect,
|
||||
mut write,
|
||||
} = client;
|
||||
|
||||
while let Ok(event) = events.recv().await {
|
||||
let mut buf = serde_json::to_vec(&event).context("error formatting event")?;
|
||||
buf.push(b'\n');
|
||||
|
||||
let res = select_biased! {
|
||||
_ = disconnect.recv().fuse() => return Ok(()),
|
||||
res = write.write_all(&buf).fuse() => res,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
// Normal client disconnection.
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||
res @ Err(_) => res.context("error writing event")?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
|
||||
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
pid: mapped.credentials().map(|c| c.pid),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
is_floating: mapped.is_floating(),
|
||||
})
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn ipc_keyboard_layouts_changed(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
let layouts = xkb.layouts();
|
||||
KeyboardLayouts {
|
||||
names: layouts
|
||||
.map(|layout| xkb.layout_name(layout).to_owned())
|
||||
.collect(),
|
||||
current_idx: xkb.active_layout().0 as u8,
|
||||
}
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
let event = Event::KeyboardLayoutsChanged { keyboard_layouts };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let idx = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
xkb.active_layout().0 as u8
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
if state.keyboard_layouts.as_ref().unwrap().current_idx == idx {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = Event::KeyboardLayoutSwitched { idx };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_layout(&mut self) {
|
||||
self.ipc_refresh_workspaces();
|
||||
self.ipc_refresh_windows();
|
||||
}
|
||||
|
||||
fn ipc_refresh_workspaces(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_workspaces");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.workspaces;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
let focused_ws_id = layout.active_workspace().map(|ws| ws.id().get());
|
||||
|
||||
// Check for workspace changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut need_workspaces_changed = false;
|
||||
for (mon, ws_idx, ws) in layout.workspaces() {
|
||||
let id = ws.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
let Some(ipc_ws) = state.workspaces.get(&id) else {
|
||||
// A new workspace was added.
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
};
|
||||
|
||||
// Check for any changes that we can't signal as individual events.
|
||||
let output_name = mon.map(|mon| mon.output_name());
|
||||
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|
||||
|| ipc_ws.name.as_ref() != ws.name()
|
||||
|| ipc_ws.output.as_ref() != output_name
|
||||
{
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let active_window_id = ws.active_window().map(|win| win.id().get());
|
||||
if ipc_ws.active_window_id != active_window_id {
|
||||
events.push(Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id: id,
|
||||
active_window_id,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this workspace became focused.
|
||||
let is_focused = Some(id) == focused_ws_id;
|
||||
if is_focused && !ipc_ws.is_focused {
|
||||
events.push(Event::WorkspaceActivated { id, focused: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this workspace became active.
|
||||
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
|
||||
if is_active && !ipc_ws.is_active {
|
||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any workspaces were removed.
|
||||
if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) {
|
||||
need_workspaces_changed = true;
|
||||
}
|
||||
|
||||
if need_workspaces_changed {
|
||||
events.clear();
|
||||
|
||||
let workspaces = layout
|
||||
.workspaces()
|
||||
.map(|(mon, ws_idx, ws)| {
|
||||
let id = ws.id().get();
|
||||
Workspace {
|
||||
id,
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name().cloned(),
|
||||
output: mon.map(|mon| mon.output_name().clone()),
|
||||
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
|
||||
is_focused: Some(id) == focused_ws_id,
|
||||
active_window_id: ws.active_window().map(|win| win.id().get()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
events.push(Event::WorkspacesChanged { workspaces });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn ipc_refresh_windows(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_windows");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.windows;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
|
||||
// Check for window changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut focused_id = None;
|
||||
layout.with_windows(|mapped, _, ws_id| {
|
||||
let id = mapped.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
if mapped.is_focused() {
|
||||
focused_id = Some(id);
|
||||
}
|
||||
|
||||
let Some(ipc_win) = state.windows.get(&id) else {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace_id = ws_id.map(|id| id.get());
|
||||
let mut changed =
|
||||
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
|
||||
|
||||
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
||||
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
||||
});
|
||||
|
||||
if changed {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
}
|
||||
|
||||
if mapped.is_focused() && !ipc_win.is_focused {
|
||||
events.push(Event::WindowFocusChanged { id: Some(id) });
|
||||
}
|
||||
});
|
||||
|
||||
// Check for closed windows.
|
||||
let mut ipc_focused_id = None;
|
||||
for (id, ipc_win) in &state.windows {
|
||||
if !seen.contains(id) {
|
||||
events.push(Event::WindowClosed { id: *id });
|
||||
}
|
||||
|
||||
if ipc_win.is_focused {
|
||||
ipc_focused_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Extra check for focus becoming None, since the checks above only work for focus becoming
|
||||
// a different window.
|
||||
if focused_id.is_none() && ipc_focused_id.is_some() {
|
||||
events.push(Event::WindowFocusChanged { id: None });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
use niri_config::layer_rule::LayerRule;
|
||||
use niri_config::Config;
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::layout::shadow::Shadow;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MappedLayer {
|
||||
/// The surface itself.
|
||||
surface: LayerSurface,
|
||||
|
||||
/// Up-to-date rules.
|
||||
rules: ResolvedLayerRules,
|
||||
|
||||
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||
block_out_buffer: SolidColorBuffer,
|
||||
|
||||
/// The shadow around the surface.
|
||||
shadow: Shadow,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
LayerSurfaceRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Shadow = ShadowRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules, config: &Config) -> Self {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = rules.shadow.resolve_against(shadow_config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
shadow: Shadow::new(shadow_config),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &Config) {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
|
||||
self.shadow.update_config(shadow_config);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, size: Size<f64, Logical>, scale: Scale<f64>) {
|
||||
// Round to physical pixels.
|
||||
let size = size.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
self.block_out_buffer.resize(size);
|
||||
|
||||
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||
// FIXME: is_active based on keyboard focus?
|
||||
self.shadow
|
||||
.update_render_elements(size, true, radius, scale.x, 1.);
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &LayerSurface {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> &ResolvedLayerRules {
|
||||
&self.rules
|
||||
}
|
||||
|
||||
/// Recomputes the resolved layer rules and returns whether they changed.
|
||||
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
|
||||
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
|
||||
if new_rules == self.rules {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.rules = new_rules;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
// FIXME: take geometry-corner-radius into account.
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&self.block_out_buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
));
|
||||
}
|
||||
|
||||
rv.normal = render_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
}
|
||||
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.normal
|
||||
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
use niri_config::layer_rule::{LayerRule, Match};
|
||||
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use smithay::desktop::LayerSurface;
|
||||
|
||||
pub mod mapped;
|
||||
pub use mapped::MappedLayer;
|
||||
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this layer surface with.
|
||||
pub opacity: Option<f32>,
|
||||
|
||||
/// Whether to block out this layer surface from certain render targets.
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
|
||||
/// Shadow overrides.
|
||||
pub shadow: ShadowRule,
|
||||
|
||||
/// Corner radius to assume this layer surface has.
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
on: false,
|
||||
offset: None,
|
||||
softness: None,
|
||||
spread: None,
|
||||
draw_behind_window: None,
|
||||
color: None,
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||
|
||||
let mut resolved = ResolvedLayerRules::empty();
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m: &Match| {
|
||||
if let Some(at_startup) = m.at_startup {
|
||||
if at_startup != is_at_startup {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
surface_matches(surface, m)
|
||||
};
|
||||
|
||||
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.excludes.iter().any(matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(x) = rule.opacity {
|
||||
resolved.opacity = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.block_out_from {
|
||||
resolved.block_out_from = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.geometry_corner_radius {
|
||||
resolved.geometry_corner_radius = Some(x);
|
||||
}
|
||||
|
||||
resolved.shadow.merge_with(&rule.shadow);
|
||||
}
|
||||
|
||||
resolved
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
|
||||
if let Some(namespace_re) = &m.namespace {
|
||||
if !namespace_re.0.is_match(surface.namespace()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::wayland::compositor::{Blocker, BlockerState};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::shader_element::ShaderRenderElement;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
||||
use crate::utils::transaction::TransactionBlocker;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClosingWindow {
|
||||
/// Contents of the window.
|
||||
buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
/// Blocked-out contents of the window.
|
||||
blocked_out_buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
/// Where the window should be blocked out from.
|
||||
block_out_from: Option<BlockOutFrom>,
|
||||
|
||||
/// Size of the window geometry.
|
||||
geo_size: Size<f64, Logical>,
|
||||
|
||||
/// Position in the workspace.
|
||||
pos: Point<f64, Logical>,
|
||||
|
||||
/// How much the texture should be offset.
|
||||
buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// How much the blocked-out texture should be offset.
|
||||
blocked_out_buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// The closing animation.
|
||||
anim_state: AnimationState,
|
||||
|
||||
/// Random seed for the shader.
|
||||
random_seed: f32,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
ClosingWindowRenderElement => {
|
||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||
Shader = ShaderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AnimationState {
|
||||
Waiting {
|
||||
/// Blocker for a transaction before starting the animation.
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
},
|
||||
Animating(Animation),
|
||||
}
|
||||
|
||||
impl AnimationState {
|
||||
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
|
||||
if blocker.state() == BlockerState::Pending {
|
||||
Self::Waiting { blocker, anim }
|
||||
} else {
|
||||
// This actually doesn't normally happen because the window is removed only after the
|
||||
// closing animation is created. Though, it does happen with disable-transactions debug
|
||||
// flag.
|
||||
Self::Animating(anim)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClosingWindow {
|
||||
pub fn new<E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
snapshot: RenderSnapshot<E, E>,
|
||||
scale: Scale<f64>,
|
||||
geo_size: Size<f64, Logical>,
|
||||
pos: Point<f64, Logical>,
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("ClosingWindow::new");
|
||||
|
||||
let mut render_to_texture = |elements: Vec<E>| -> anyhow::Result<_> {
|
||||
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
||||
renderer,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
&elements,
|
||||
)
|
||||
.context("error rendering to texture")?;
|
||||
|
||||
let buffer = TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
|
||||
Ok((buffer, offset))
|
||||
};
|
||||
|
||||
let (buffer, buffer_offset) =
|
||||
render_to_texture(snapshot.contents).context("error rendering contents")?;
|
||||
let (blocked_out_buffer, blocked_out_buffer_offset) =
|
||||
render_to_texture(snapshot.blocked_out_contents)
|
||||
.context("error rendering blocked-out contents")?;
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
blocked_out_buffer,
|
||||
block_out_from: snapshot.block_out_from,
|
||||
geo_size,
|
||||
pos,
|
||||
buffer_offset,
|
||||
blocked_out_buffer_offset,
|
||||
anim_state: AnimationState::new(blocker, anim),
|
||||
random_seed: fastrand::f32(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
match &mut self.anim_state {
|
||||
AnimationState::Waiting { blocker, anim } => {
|
||||
if blocker.state() != BlockerState::Pending {
|
||||
let anim = anim.restarted(0., 1., 0.);
|
||||
self.anim_state = AnimationState::Animating(anim);
|
||||
}
|
||||
}
|
||||
AnimationState::Animating(_anim) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => true,
|
||||
AnimationState::Animating(anim) => !anim.is_done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> ClosingWindowRenderElement {
|
||||
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
|
||||
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
|
||||
} else {
|
||||
(&self.buffer, self.buffer_offset)
|
||||
};
|
||||
|
||||
let anim = match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => {
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), 1.);
|
||||
|
||||
let mut location = self.pos + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
return elem.into();
|
||||
}
|
||||
AnimationState::Animating(anim) => anim,
|
||||
};
|
||||
|
||||
let progress = anim.value();
|
||||
let clamped_progress = anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
|
||||
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
|
||||
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
|
||||
|
||||
// Round to physical pixels relative to the view position. This is similar to what
|
||||
// happens when rendering normal windows.
|
||||
let relative = self.pos - view_rect.loc;
|
||||
let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
let geo_loc = Vec2::new(pos.x as f32, pos.y as f32);
|
||||
let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32);
|
||||
|
||||
let input_to_geo = Mat3::from_scale(area_size / geo_size)
|
||||
* Mat3::from_translation((area_loc - geo_loc) / area_size);
|
||||
|
||||
let tex_scale = self.buffer.texture_scale();
|
||||
let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32);
|
||||
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
|
||||
let tex_size = self.buffer.texture().size();
|
||||
let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale;
|
||||
|
||||
let geo_to_tex =
|
||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||
|
||||
return ShaderRenderElement::new(
|
||||
ProgramType::Close,
|
||||
view_rect.size,
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(Point::from((0., 0.)))
|
||||
.into();
|
||||
}
|
||||
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
1. - clamped_progress as f32,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
|
||||
let center = self.geo_size.to_point().downscale(2.);
|
||||
let elem = RescaleRenderElement::from_element(
|
||||
elem,
|
||||
(center - offset).to_physical_precise_round(scale),
|
||||
((1. - clamped_progress) / 5. + 0.8).max(0.),
|
||||
);
|
||||
|
||||
let mut location = self.pos + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
elem.into()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+64
-220
@@ -1,272 +1,116 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::utils::round_logical_in_physical_max1;
|
||||
use crate::config::{self, Color};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FocusRing {
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
locations: [Point<f64, Logical>; 8],
|
||||
sizes: [Size<f64, Logical>; 8],
|
||||
borders: [BorderRenderElement; 8],
|
||||
full_size: Size<f64, Logical>,
|
||||
buffers: [SolidColorBuffer; 4],
|
||||
locations: [Point<i32, Logical>; 4],
|
||||
is_off: bool,
|
||||
is_border: bool,
|
||||
use_border_shader: bool,
|
||||
config: niri_config::FocusRing,
|
||||
width: i32,
|
||||
active_color: Color,
|
||||
inactive_color: Color,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
FocusRingRenderElement => {
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Gradient = BorderRenderElement,
|
||||
}
|
||||
}
|
||||
pub type FocusRingRenderElement = SolidColorRenderElement;
|
||||
|
||||
impl FocusRing {
|
||||
pub fn new(config: niri_config::FocusRing) -> Self {
|
||||
pub fn new(config: config::FocusRing) -> Self {
|
||||
Self {
|
||||
buffers: Default::default(),
|
||||
locations: Default::default(),
|
||||
sizes: Default::default(),
|
||||
borders: Default::default(),
|
||||
full_size: Default::default(),
|
||||
is_off: config.off,
|
||||
is_border: false,
|
||||
use_border_shader: false,
|
||||
config,
|
||||
width: config.width.into(),
|
||||
active_color: config.active_color,
|
||||
inactive_color: config.inactive_color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
||||
self.config = config;
|
||||
pub fn update_config(&mut self, config: config::FocusRing) {
|
||||
self.is_off = config.off;
|
||||
self.width = config.width.into();
|
||||
self.active_color = config.active_color;
|
||||
self.inactive_color = config.inactive_color;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.borders {
|
||||
elem.damage_all();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_render_elements(
|
||||
pub fn update(
|
||||
&mut self,
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
win_pos: Point<i32, Logical>,
|
||||
win_size: Size<i32, Logical>,
|
||||
is_border: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
alpha: f32,
|
||||
) {
|
||||
// let scale = scale * extra_visual_scale;
|
||||
// let width = self.config.width.0 / extra_visual_scale;
|
||||
let width = round_logical_in_physical_max1(scale, self.config.width.0);
|
||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
self.config.active_color
|
||||
} else {
|
||||
self.config.inactive_color
|
||||
};
|
||||
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color.to_array_premul());
|
||||
}
|
||||
|
||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||
|
||||
let gradient = if is_active {
|
||||
self.config.active_gradient
|
||||
} else {
|
||||
self.config.inactive_gradient
|
||||
};
|
||||
|
||||
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
||||
|
||||
// Set the defaults for solid color + rounded corners.
|
||||
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
|
||||
|
||||
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
|
||||
let gradient_area = match gradient.relative_to {
|
||||
GradientRelativeTo::Window => full_rect,
|
||||
GradientRelativeTo::WorkspaceView => view_rect,
|
||||
};
|
||||
|
||||
let rounded_corner_border_width = if self.is_border {
|
||||
// HACK: increase the border width used for the inner rounded corners a tiny bit to
|
||||
// reduce background bleed.
|
||||
width as f32 + 0.5
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let ceil = |logical: f64| (logical * scale).ceil() / scale;
|
||||
|
||||
// All of this stuff should end up aligned to physical pixels because:
|
||||
// * Window size and border width are rounded to physical pixels before being passed to this
|
||||
// function.
|
||||
// * We will ceil the corner radii below.
|
||||
// * We do not divide anything, only add, subtract and multiply by integers.
|
||||
// * At rendering time, tile positions are rounded to physical pixels.
|
||||
|
||||
if is_border {
|
||||
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
|
||||
let top_right = f64::min(
|
||||
self.full_size.w - top_left,
|
||||
f64::max(width, ceil(f64::from(radius.top_right))),
|
||||
);
|
||||
let bottom_left = f64::min(
|
||||
self.full_size.h - top_left,
|
||||
f64::max(width, ceil(f64::from(radius.bottom_left))),
|
||||
);
|
||||
let bottom_right = f64::min(
|
||||
self.full_size.h - top_right,
|
||||
f64::min(
|
||||
self.full_size.w - bottom_left,
|
||||
f64::max(width, ceil(f64::from(radius.bottom_right))),
|
||||
),
|
||||
);
|
||||
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));
|
||||
|
||||
// Top edge.
|
||||
self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width));
|
||||
self.locations[0] = Point::from((-width + top_left, -width));
|
||||
|
||||
// Bottom edge.
|
||||
self.sizes[1] =
|
||||
Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width));
|
||||
self.locations[1] = Point::from((-width + bottom_left, win_size.h));
|
||||
|
||||
// Left edge.
|
||||
self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left));
|
||||
self.locations[2] = Point::from((-width, -width + top_left));
|
||||
|
||||
// Right edge.
|
||||
self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right));
|
||||
self.locations[3] = Point::from((win_size.w, -width + top_right));
|
||||
|
||||
// Top-left corner.
|
||||
self.sizes[4] = Size::from((top_left, top_left));
|
||||
self.locations[4] = Point::from((-width, -width));
|
||||
|
||||
// Top-right corner.
|
||||
self.sizes[5] = Size::from((top_right, top_right));
|
||||
self.locations[5] = Point::from((win_size.w + width - top_right, -width));
|
||||
|
||||
// Bottom-right corner.
|
||||
self.sizes[6] = Size::from((bottom_right, bottom_right));
|
||||
self.locations[6] = Point::from((
|
||||
win_size.w + width - bottom_right,
|
||||
win_size.h + width - bottom_right,
|
||||
));
|
||||
|
||||
// Bottom-left corner.
|
||||
self.sizes[7] = Size::from((bottom_left, bottom_left));
|
||||
self.locations[7] = Point::from((-width, win_size.h + width - bottom_left));
|
||||
|
||||
for (buf, size) in zip(&mut self.buffers, self.sizes) {
|
||||
buf.resize(size);
|
||||
}
|
||||
|
||||
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
||||
border.update(
|
||||
size,
|
||||
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::new(full_rect.loc - loc, full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
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));
|
||||
} else {
|
||||
self.sizes[0] = self.full_size;
|
||||
self.buffers[0].resize(self.sizes[0]);
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
|
||||
self.borders[0].update(
|
||||
self.sizes[0],
|
||||
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
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.is_border = is_border;
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 8>::new();
|
||||
pub fn set_active(&mut self, is_active: bool) {
|
||||
let color = if is_active {
|
||||
self.active_color.into()
|
||||
} else {
|
||||
self.inactive_color.into()
|
||||
};
|
||||
|
||||
if self.config.off {
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 4>::new();
|
||||
|
||||
if self.is_off {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let border_width = -self.locations[0].y;
|
||||
|
||||
// If drawing as a border with width = 0, then there's nothing to draw.
|
||||
if self.is_border && border_width == 0. {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
|
||||
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
|
||||
let elem = if self.use_border_shader && has_border_shader {
|
||||
border.clone().with_location(location).into()
|
||||
} else {
|
||||
let alpha = border.alpha();
|
||||
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||
.into()
|
||||
};
|
||||
let mut push = |buffer, location: Point<i32, Logical>| {
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.push(elem);
|
||||
};
|
||||
|
||||
if self.is_border {
|
||||
for ((buf, border), loc) in zip(zip(&self.buffers, &self.borders), self.locations) {
|
||||
push(buf, border, location + loc);
|
||||
for (buf, loc) in zip(&self.buffers, self.locations) {
|
||||
push(buf, loc);
|
||||
}
|
||||
} else {
|
||||
push(
|
||||
&self.buffers[0],
|
||||
&self.borders[0],
|
||||
location + self.locations[0],
|
||||
);
|
||||
push(&self.buffers[0], self.locations[0]);
|
||||
}
|
||||
|
||||
rv.into_iter()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 {
|
||||
self.config.width.0
|
||||
pub fn width(&self) -> i32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn is_off(&self) -> bool {
|
||||
self.config.off
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &niri_config::FocusRing {
|
||||
&self.config
|
||||
self.is_off
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use niri_config::{CornerRadius, FloatOrInt};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertHintElement {
|
||||
inner: FocusRing,
|
||||
}
|
||||
|
||||
pub type InsertHintRenderElement = FocusRingRenderElement;
|
||||
|
||||
impl InsertHintElement {
|
||||
pub fn new(config: niri_config::InsertHint) -> Self {
|
||||
Self {
|
||||
inner: FocusRing::new(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::InsertHint) {
|
||||
self.inner.update_config(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.inner.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale, 1.);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
}
|
||||
}
|
||||
+1319
-4292
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user