mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b74f4a3a | |||
| b94b0c7fa4 | |||
| cbd066ab68 | |||
| bccde351fb | |||
| beaffb1b97 | |||
| 385454378b | |||
| 18f06a7acd | |||
| 6e23073019 | |||
| a9fcbf81eb | |||
| a99f34cba8 | |||
| bd2277fa25 | |||
| 67182129ff | |||
| d6b116d229 | |||
| c20a843ab2 | |||
| 1b752fe08f | |||
| 89f74aae98 | |||
| 5e553c2679 | |||
| cabf712821 | |||
| 0931447ec1 | |||
| a388c25795 | |||
| 5c4d9824a4 | |||
| ca4ee5ae25 | |||
| 93e16a6582 | |||
| 3486fa5536 | |||
| c022d74c82 | |||
| e68641c0a7 | |||
| 2a892ef511 | |||
| 90c6721e97 | |||
| e5cd9e9307 | |||
| 573dca10cc | |||
| 577fba82e5 | |||
| b9116c579a | |||
| d8dcadc5b2 | |||
| 6424a2738d | |||
| 753a90430a | |||
| f9085db564 | |||
| 49ce791d13 | |||
| 4b8e04da04 | |||
| 026ad8f377 | |||
| 0761401650 | |||
| 3360517f62 | |||
| 9896fd67a0 | |||
| 15ec699fbb | |||
| a1cc39a437 | |||
| 738d9a2b40 | |||
| 68752db51b | |||
| d4929b8e18 | |||
| 93c547f749 | |||
| e2b91c0c1c | |||
| 322b5cbac7 | |||
| 592791611a | |||
| d073d2ab3d | |||
| b2298db5c5 | |||
| baa6263cbe | |||
| 795da53d53 | |||
| 122afff7d1 | |||
| d2a4e6a0cb | |||
| 8916b18c6b | |||
| b0d0fce5f3 | |||
| 3dc4a5fdac | |||
| 1706a46b2b | |||
| 3789d85588 | |||
| 3a23417e98 | |||
| 6bb83757ee | |||
| b62a07956a | |||
| 96016790b2 | |||
| bf978fe98d | |||
| 57521c69c3 | |||
| da826e42aa | |||
| b824cf90ab | |||
| 7a4bb8ba8a | |||
| 72c8f569ac | |||
| 798d9c55df | |||
| 05613eed1e | |||
| b23dd4b800 | |||
| 1f72089a46 | |||
| fbe9020915 | |||
| 2036116f16 | |||
| 9afd728ae9 | |||
| e51268a39e | |||
| 0a715ce155 | |||
| 89ac958670 | |||
| 2e50f8dee0 | |||
| 7052f0129e | |||
| 962e159db6 | |||
| 11bff3a2f1 | |||
| 15606304f2 | |||
| 85eac9d9d0 | |||
| d3f4583c90 | |||
| fefb1cccd6 | |||
| deef52519a | |||
| 59ff331597 | |||
| b813f99abd | |||
| d9b9cec8b8 | |||
| 597ea62d17 | |||
| 51243a0a50 | |||
| 0ebcc3e0d6 | |||
| 64c85d865e | |||
| 367e4955ea | |||
| dd967554d1 | |||
| 6d7c220137 | |||
| d77aac1afa | |||
| 837a0a20fb | |||
| ecdf756b55 | |||
| 73f3c160b2 | |||
| 5f99eb13ab | |||
| 20326b093c | |||
| 467d92a4b4 | |||
| 15bb69c0b9 | |||
| adfbfdffb3 | |||
| 087ed260c5 | |||
| f5642ab733 | |||
| ab9706cb30 | |||
| 05f2a3709b | |||
| 743173ef64 | |||
| cbbb7a26fc | |||
| 18566e3366 | |||
| df48337d83 | |||
| f5e9b40140 | |||
| 5cacd03e85 |
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug or a crash
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||
|
||||
### System Information
|
||||
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
|
||||
* GPU:
|
||||
|
||||
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
|
||||
* CPU:
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||
about: Ideas for new features and functionality (start a Discussion)
|
||||
+55
-21
@@ -24,6 +24,7 @@ jobs:
|
||||
|
||||
name: test - ${{ matrix.configuration }}
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -32,15 +33,10 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -56,17 +52,18 @@ jobs:
|
||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
||||
|
||||
- name: Build Tests
|
||||
run: cargo test --no-run --all ${{ matrix.release-flag }}
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
|
||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
clippy:
|
||||
visual-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: clippy
|
||||
name: visual tests
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -75,15 +72,37 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
|
||||
- name: Install Rust
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --package niri-visual-tests
|
||||
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: clippy
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install stable --profile minimal --component clippy
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -119,8 +138,23 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo dnf update -y
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build
|
||||
- run: cargo build --all
|
||||
|
||||
nix:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v4
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
continue-on-error: true
|
||||
|
||||
Generated
+550
-230
File diff suppressed because it is too large
Load Diff
+28
-20
@@ -1,5 +1,8 @@
|
||||
[workspace]
|
||||
members = ["niri-visual-tests"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0-beta.1"
|
||||
version = "0.1.1"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -7,19 +10,23 @@ edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.79"
|
||||
bitflags = "2.4.2"
|
||||
directories = "5.0.1"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.16.5", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
git = "https://github.com/YaLTeR/smithay.git"
|
||||
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
git = "https://github.com/YaLTeR/smithay.git"
|
||||
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
[package]
|
||||
@@ -35,38 +42,38 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.79" }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.4"
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
async-channel = { version = "2.2.0", optional = true }
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
bitflags = "2.4.2"
|
||||
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
|
||||
clap = { version = "4.4.18", features = ["derive", "string"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.152"
|
||||
libc = "0.2.153"
|
||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
logind-zbus = { version = "3.1.2", optional = true }
|
||||
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
|
||||
niri-config = { version = "0.1.1", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.1", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "4.10.0", optional = true }
|
||||
pangocairo = "0.18.0"
|
||||
pipewire = { version = "0.7.2", optional = true }
|
||||
pangocairo = "0.19.1"
|
||||
pipewire = { version = "0.8.0", optional = true }
|
||||
png = "0.17.11"
|
||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.13"
|
||||
profiling = "1.0.14"
|
||||
sd-notify = "0.4.1"
|
||||
serde.workspace = true
|
||||
serde_json = "1.0.111"
|
||||
serde_json = "1.0.113"
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.0", optional = true }
|
||||
xcursor = "0.3.5"
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
zbus = { version = "3.15.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -80,6 +87,7 @@ features = [
|
||||
"backend_winit",
|
||||
"desktop",
|
||||
"renderer_gl",
|
||||
"renderer_pixman",
|
||||
"renderer_multi",
|
||||
"use_system_lib",
|
||||
"wayland_frontend",
|
||||
@@ -92,7 +100,7 @@ proptest-derive = "0.4.0"
|
||||
[features]
|
||||
default = ["dbus", "xdp-gnome-screencast"]
|
||||
# Enables DBus support (required for xdp-gnome and power button inhibiting).
|
||||
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
|
||||
dbus = ["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.
|
||||
@@ -108,7 +116,7 @@ lto = "thin"
|
||||
debug = false
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.0~beta.1"
|
||||
version = "0.1.1"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -16,9 +16,12 @@ Opening a new window never causes existing windows to resize.
|
||||
Every monitor has its own separate window strip.
|
||||
Windows can never "overflow" onto an adjacent monitor.
|
||||
|
||||
Since windows go left-to-right horizontally, workspaces are arranged vertically.
|
||||
Workspaces are dynamic and arranged vertically.
|
||||
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
||||
|
||||
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
|
||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||
|
||||
## Features
|
||||
|
||||
- Scrollable tiling
|
||||
@@ -29,13 +32,17 @@ Every monitor has an independent set of workspaces, and there's always one empty
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- Live-reloading config
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
|
||||
## Status
|
||||
|
||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||
Feel free to give niri a try.
|
||||
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
Note that NVIDIA GPUs might have rendering issues.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -44,25 +51,21 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
|
||||
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
||||
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||
|
||||
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
|
||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||
|
||||
## Building
|
||||
|
||||
> [!TIP]
|
||||
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||
>
|
||||
> For NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||
> NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||
>
|
||||
> For Arch users, there's an AUR package: https://aur.archlinux.org/packages/niri
|
||||
|
||||
First, install the dependencies for your distribution.
|
||||
|
||||
- Ubuntu:
|
||||
- Ubuntu 23.10:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
```
|
||||
|
||||
- Fedora:
|
||||
@@ -71,7 +74,9 @@ First, install the dependencies for your distribution.
|
||||
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
|
||||
```
|
||||
|
||||
Next, build niri with `cargo build --release`.
|
||||
Next, get latest stable Rust: https://rustup.rs/
|
||||
|
||||
Then, build niri with `cargo build --release`.
|
||||
|
||||
### NixOS/Nix
|
||||
|
||||
@@ -202,4 +207,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[mako]: https://github.com/emersion/mako
|
||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
|
||||
Generated
+18
-18
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1702918879,
|
||||
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
|
||||
"lastModified": 1707685877,
|
||||
"narHash": "sha256-XoXRS+5whotelr1rHiZle5t5hDg9kpguS5yk8c8qzOc=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
|
||||
"rev": "2c653e4478476a52c6aa3ac0495e4dea7449ea0e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701411808,
|
||||
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
|
||||
"lastModified": 1706768574,
|
||||
"narHash": "sha256-4o6TMpzBHO659EiJTzd/EGQGUDdbgwKwhqf3u6b23U8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
|
||||
"rev": "668102037129923cd0fc239d864fce71eabdc6a3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,11 +47,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -62,11 +62,11 @@
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1701697642,
|
||||
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
|
||||
"lastModified": 1705332318,
|
||||
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
|
||||
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,11 +77,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1702900294,
|
||||
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
|
||||
"lastModified": 1707619277,
|
||||
"narHash": "sha256-vKnYD5GMQbNQyyQm4wRlqi+5n0/F1hnvqSQgaBy4BqY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
|
||||
"rev": "f3a93440fbfff8a74350f4791332a19282cc6dc8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -103,11 +103,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1701372675,
|
||||
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
|
||||
"lastModified": 1706735270,
|
||||
"narHash": "sha256-IJk+UitcJsxzMQWm9pa1ZbJBriQ4ginXOlPyVq+Cu40=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
|
||||
"rev": "42cb1a2bd79af321b0cc503d2960b73f34e2f92b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -39,16 +39,12 @@
|
||||
pname = "niri";
|
||||
version = self.rev or "dirty";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ./.;
|
||||
include = [
|
||||
./src
|
||||
./niri-config
|
||||
./niri-ipc
|
||||
./Cargo.toml
|
||||
./Cargo.lock
|
||||
./resources
|
||||
];
|
||||
src = nixpkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = path: type:
|
||||
(builtins.match "resources" path == null) ||
|
||||
((craneLib.filterCargoSources path type) &&
|
||||
(builtins.match "niri-visual-tests" path == null));
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
|
||||
@@ -11,6 +11,7 @@ repository.workspace = true
|
||||
bitflags.workspace = true
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
smithay.workspace = true
|
||||
niri-ipc = { version = "0.1.1", path = "../niri-ipc" }
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
+299
-103
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
|
||||
use niri_ipc::{LayoutSwitchTarget, SizeChange};
|
||||
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
|
||||
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
||||
use smithay::input::keyboard::{Keysym, XkbConfig};
|
||||
@@ -36,6 +37,8 @@ pub struct Config {
|
||||
#[knuffel(child, default)]
|
||||
pub hotkey_overlay: HotkeyOverlay,
|
||||
#[knuffel(child, default)]
|
||||
pub animations: Animations,
|
||||
#[knuffel(child, default)]
|
||||
pub binds: Binds,
|
||||
#[knuffel(child, default)]
|
||||
pub debug: DebugConfig,
|
||||
@@ -124,6 +127,8 @@ pub struct Touchpad {
|
||||
#[knuffel(child)]
|
||||
pub dwt: bool,
|
||||
#[knuffel(child)]
|
||||
pub dwtp: bool,
|
||||
#[knuffel(child)]
|
||||
pub natural_scroll: bool,
|
||||
#[knuffel(child, unwrap(argument), default)]
|
||||
pub accel_speed: f64,
|
||||
@@ -187,6 +192,8 @@ pub struct Output {
|
||||
pub name: String,
|
||||
#[knuffel(child, unwrap(argument), default = 1.)]
|
||||
pub scale: f64,
|
||||
#[knuffel(child, unwrap(argument, str), default = Transform::Normal)]
|
||||
pub transform: Transform,
|
||||
#[knuffel(child)]
|
||||
pub position: Option<Position>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
@@ -199,12 +206,62 @@ impl Default for Output {
|
||||
off: false,
|
||||
name: String::new(),
|
||||
scale: 1.,
|
||||
transform: Transform::Normal,
|
||||
position: None,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Output transform, which goes counter-clockwise.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Transform {
|
||||
Normal,
|
||||
_90,
|
||||
_180,
|
||||
_270,
|
||||
Flipped,
|
||||
Flipped90,
|
||||
Flipped180,
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
impl FromStr for Transform {
|
||||
type Err = miette::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"normal" => Ok(Self::Normal),
|
||||
"90" => Ok(Self::_90),
|
||||
"180" => Ok(Self::_180),
|
||||
"270" => Ok(Self::_270),
|
||||
"flipped" => Ok(Self::Flipped),
|
||||
"flipped-90" => Ok(Self::Flipped90),
|
||||
"flipped-180" => Ok(Self::Flipped180),
|
||||
"flipped-270" => Ok(Self::Flipped270),
|
||||
_ => Err(miette!(concat!(
|
||||
r#"invalid transform, can be "90", "180", "270", "#,
|
||||
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transform> for smithay::utils::Transform {
|
||||
fn from(value: Transform) -> Self {
|
||||
match value {
|
||||
Transform::Normal => Self::Normal,
|
||||
Transform::_90 => Self::_90,
|
||||
Transform::_180 => Self::_180,
|
||||
Transform::_270 => Self::_270,
|
||||
Transform::Flipped => Self::Flipped,
|
||||
Transform::Flipped90 => Self::Flipped90,
|
||||
Transform::Flipped180 => Self::Flipped180,
|
||||
Transform::Flipped270 => Self::Flipped270,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Position {
|
||||
#[knuffel(property)]
|
||||
@@ -224,8 +281,8 @@ pub struct Mode {
|
||||
pub struct Layout {
|
||||
#[knuffel(child, default)]
|
||||
pub focus_ring: FocusRing,
|
||||
#[knuffel(child, default = default_border())]
|
||||
pub border: FocusRing,
|
||||
#[knuffel(child, default)]
|
||||
pub border: Border,
|
||||
#[knuffel(child, unwrap(children), default)]
|
||||
pub preset_column_widths: Vec<PresetWidth>,
|
||||
#[knuffel(child)]
|
||||
@@ -248,11 +305,11 @@ pub struct SpawnAtStartup {
|
||||
pub struct FocusRing {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, unwrap(argument), default = 4)]
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().width)]
|
||||
pub width: u16,
|
||||
#[knuffel(child, default = Color::new(127, 200, 255, 255))]
|
||||
#[knuffel(child, default = Self::default().active_color)]
|
||||
pub active_color: Color,
|
||||
#[knuffel(child, default = Color::new(80, 80, 80, 255))]
|
||||
#[knuffel(child, default = Self::default().inactive_color)]
|
||||
pub inactive_color: Color,
|
||||
}
|
||||
|
||||
@@ -267,12 +324,37 @@ impl Default for FocusRing {
|
||||
}
|
||||
}
|
||||
|
||||
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(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Border {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().width)]
|
||||
pub width: u16,
|
||||
#[knuffel(child, default = Self::default().active_color)]
|
||||
pub active_color: Color,
|
||||
#[knuffel(child, default = Self::default().inactive_color)]
|
||||
pub inactive_color: Color,
|
||||
}
|
||||
|
||||
impl Default for Border {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: true,
|
||||
width: 4,
|
||||
active_color: Color::new(255, 200, 127, 255),
|
||||
inactive_color: Color::new(80, 80, 80, 255),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Border> for FocusRing {
|
||||
fn from(value: Border) -> Self {
|
||||
Self {
|
||||
off: value.off,
|
||||
width: value.width,
|
||||
active_color: value.active_color,
|
||||
inactive_color: value.inactive_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +378,8 @@ impl Color {
|
||||
|
||||
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.)
|
||||
let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.);
|
||||
[r * a, g * a, b * a, a]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +427,89 @@ pub struct HotkeyOverlay {
|
||||
pub skip_at_startup: bool,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Animations {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, unwrap(argument), default = 1.)]
|
||||
pub slowdown: f64,
|
||||
#[knuffel(child, default = Animation::default_workspace_switch())]
|
||||
pub workspace_switch: Animation,
|
||||
#[knuffel(child, default = Animation::default_horizontal_view_movement())]
|
||||
pub horizontal_view_movement: Animation,
|
||||
#[knuffel(child, default = Animation::default_window_open())]
|
||||
pub window_open: Animation,
|
||||
#[knuffel(child, default = Animation::default_config_notification_open_close())]
|
||||
pub config_notification_open_close: Animation,
|
||||
}
|
||||
|
||||
impl Default for Animations {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
slowdown: 1.,
|
||||
workspace_switch: Animation::default_workspace_switch(),
|
||||
horizontal_view_movement: Animation::default_horizontal_view_movement(),
|
||||
window_open: Animation::default_window_open(),
|
||||
config_notification_open_close: Animation::default_config_notification_open_close(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Animation {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub duration_ms: Option<u32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub curve: Option<AnimationCurve>,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub const fn unfilled() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
duration_ms: None,
|
||||
curve: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
duration_ms: Some(250),
|
||||
curve: Some(AnimationCurve::EaseOutCubic),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_workspace_switch() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub const fn default_horizontal_view_movement() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub const fn default_config_notification_open_close() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub const fn default_window_open() -> Self {
|
||||
Self {
|
||||
duration_ms: Some(150),
|
||||
curve: Some(AnimationCurve::EaseOutExpo),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum AnimationCurve {
|
||||
EaseOutCubic,
|
||||
EaseOutExpo,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
|
||||
|
||||
@@ -372,9 +538,10 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
// Remember to add new actions to the CLI enum too.
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||
pub enum Action {
|
||||
Quit,
|
||||
Quit(#[knuffel(property(name = "skip-confirmation"), default)] bool),
|
||||
#[knuffel(skip)]
|
||||
ChangeVt(i32),
|
||||
Suspend,
|
||||
@@ -406,6 +573,8 @@ pub enum Action {
|
||||
MoveWindowUp,
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
ConsumeOrExpelWindowLeft,
|
||||
ConsumeOrExpelWindowRight,
|
||||
ConsumeWindowIntoColumn,
|
||||
ExpelWindowFromColumn,
|
||||
CenterColumn,
|
||||
@@ -436,28 +605,88 @@ pub enum Action {
|
||||
SwitchPresetColumnWidth,
|
||||
MaximizeColumn,
|
||||
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
|
||||
SwitchLayout(#[knuffel(argument)] LayoutAction),
|
||||
SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget),
|
||||
ShowHotkeyOverlay,
|
||||
MoveWorkspaceToMonitorLeft,
|
||||
MoveWorkspaceToMonitorRight,
|
||||
MoveWorkspaceToMonitorDown,
|
||||
MoveWorkspaceToMonitorUp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SizeChange {
|
||||
SetFixed(i32),
|
||||
SetProportion(f64),
|
||||
AdjustFixed(i32),
|
||||
AdjustProportion(f64),
|
||||
impl From<niri_ipc::Action> for Action {
|
||||
fn from(value: niri_ipc::Action) -> Self {
|
||||
match value {
|
||||
niri_ipc::Action::Quit { skip_confirmation } => Self::Quit(skip_confirmation),
|
||||
niri_ipc::Action::PowerOffMonitors => Self::PowerOffMonitors,
|
||||
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
|
||||
niri_ipc::Action::Screenshot => Self::Screenshot,
|
||||
niri_ipc::Action::ScreenshotScreen => Self::ScreenshotScreen,
|
||||
niri_ipc::Action::ScreenshotWindow => Self::ScreenshotWindow,
|
||||
niri_ipc::Action::CloseWindow => Self::CloseWindow,
|
||||
niri_ipc::Action::FullscreenWindow => Self::FullscreenWindow,
|
||||
niri_ipc::Action::FocusColumnLeft => Self::FocusColumnLeft,
|
||||
niri_ipc::Action::FocusColumnRight => Self::FocusColumnRight,
|
||||
niri_ipc::Action::FocusColumnFirst => Self::FocusColumnFirst,
|
||||
niri_ipc::Action::FocusColumnLast => Self::FocusColumnLast,
|
||||
niri_ipc::Action::FocusWindowDown => Self::FocusWindowDown,
|
||||
niri_ipc::Action::FocusWindowUp => Self::FocusWindowUp,
|
||||
niri_ipc::Action::FocusWindowOrWorkspaceDown => Self::FocusWindowOrWorkspaceDown,
|
||||
niri_ipc::Action::FocusWindowOrWorkspaceUp => Self::FocusWindowOrWorkspaceUp,
|
||||
niri_ipc::Action::MoveColumnLeft => Self::MoveColumnLeft,
|
||||
niri_ipc::Action::MoveColumnRight => Self::MoveColumnRight,
|
||||
niri_ipc::Action::MoveColumnToFirst => Self::MoveColumnToFirst,
|
||||
niri_ipc::Action::MoveColumnToLast => Self::MoveColumnToLast,
|
||||
niri_ipc::Action::MoveWindowDown => Self::MoveWindowDown,
|
||||
niri_ipc::Action::MoveWindowUp => Self::MoveWindowUp,
|
||||
niri_ipc::Action::MoveWindowDownOrToWorkspaceDown => {
|
||||
Self::MoveWindowDownOrToWorkspaceDown
|
||||
}
|
||||
niri_ipc::Action::MoveWindowUpOrToWorkspaceUp => Self::MoveWindowUpOrToWorkspaceUp,
|
||||
niri_ipc::Action::ConsumeOrExpelWindowLeft => Self::ConsumeOrExpelWindowLeft,
|
||||
niri_ipc::Action::ConsumeOrExpelWindowRight => Self::ConsumeOrExpelWindowRight,
|
||||
niri_ipc::Action::ConsumeWindowIntoColumn => Self::ConsumeWindowIntoColumn,
|
||||
niri_ipc::Action::ExpelWindowFromColumn => Self::ExpelWindowFromColumn,
|
||||
niri_ipc::Action::CenterColumn => Self::CenterColumn,
|
||||
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
|
||||
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
|
||||
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
|
||||
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
|
||||
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
|
||||
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
|
||||
niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown,
|
||||
niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp,
|
||||
niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index),
|
||||
niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown,
|
||||
niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp,
|
||||
niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft,
|
||||
niri_ipc::Action::FocusMonitorRight => Self::FocusMonitorRight,
|
||||
niri_ipc::Action::FocusMonitorDown => Self::FocusMonitorDown,
|
||||
niri_ipc::Action::FocusMonitorUp => Self::FocusMonitorUp,
|
||||
niri_ipc::Action::MoveWindowToMonitorLeft => Self::MoveWindowToMonitorLeft,
|
||||
niri_ipc::Action::MoveWindowToMonitorRight => Self::MoveWindowToMonitorRight,
|
||||
niri_ipc::Action::MoveWindowToMonitorDown => Self::MoveWindowToMonitorDown,
|
||||
niri_ipc::Action::MoveWindowToMonitorUp => Self::MoveWindowToMonitorUp,
|
||||
niri_ipc::Action::MoveColumnToMonitorLeft => Self::MoveColumnToMonitorLeft,
|
||||
niri_ipc::Action::MoveColumnToMonitorRight => Self::MoveColumnToMonitorRight,
|
||||
niri_ipc::Action::MoveColumnToMonitorDown => Self::MoveColumnToMonitorDown,
|
||||
niri_ipc::Action::MoveColumnToMonitorUp => Self::MoveColumnToMonitorUp,
|
||||
niri_ipc::Action::SetWindowHeight { change } => Self::SetWindowHeight(change),
|
||||
niri_ipc::Action::SwitchPresetColumnWidth => Self::SwitchPresetColumnWidth,
|
||||
niri_ipc::Action::MaximizeColumn => Self::MaximizeColumn,
|
||||
niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change),
|
||||
niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout),
|
||||
niri_ipc::Action::ShowHotkeyOverlay => Self::ShowHotkeyOverlay,
|
||||
niri_ipc::Action::MoveWorkspaceToMonitorLeft => Self::MoveWorkspaceToMonitorLeft,
|
||||
niri_ipc::Action::MoveWorkspaceToMonitorRight => Self::MoveWorkspaceToMonitorRight,
|
||||
niri_ipc::Action::MoveWorkspaceToMonitorDown => Self::MoveWorkspaceToMonitorDown,
|
||||
niri_ipc::Action::MoveWorkspaceToMonitorUp => Self::MoveWorkspaceToMonitorUp,
|
||||
niri_ipc::Action::ToggleDebugTint => Self::ToggleDebugTint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LayoutAction {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, PartialEq)]
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
pub struct DebugConfig {
|
||||
#[knuffel(child, unwrap(argument), default = 1.)]
|
||||
pub animation_slowdown: f64,
|
||||
#[knuffel(child)]
|
||||
pub dbus_interfaces_in_non_session_instances: bool,
|
||||
#[knuffel(child)]
|
||||
@@ -472,20 +701,6 @@ pub struct DebugConfig {
|
||||
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: &Path) -> miette::Result<Self> {
|
||||
let _span = tracy_client::span!("Config::load");
|
||||
@@ -588,58 +803,6 @@ impl FromStr for Key {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SizeChange {
|
||||
type Err = miette::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once('%') {
|
||||
Some((value, empty)) => {
|
||||
if !empty.is_empty() {
|
||||
return Err(miette!("trailing characters after '%' are not allowed"));
|
||||
}
|
||||
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.context("error parsing value")?;
|
||||
Ok(Self::AdjustProportion(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.context("error parsing value")?;
|
||||
Ok(Self::SetProportion(value))
|
||||
}
|
||||
None => Err(miette!("value is missing")),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let value = s;
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.context("error parsing value")?;
|
||||
Ok(Self::AdjustFixed(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.context("error parsing value")?;
|
||||
Ok(Self::SetFixed(value))
|
||||
}
|
||||
None => Err(miette!("value is missing")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AccelProfile {
|
||||
type Err = miette::Error;
|
||||
|
||||
@@ -706,6 +869,7 @@ mod tests {
|
||||
touchpad {
|
||||
tap
|
||||
dwt
|
||||
dwtp
|
||||
accel-speed 0.2
|
||||
accel-profile "flat"
|
||||
tap-button-map "left-middle-right"
|
||||
@@ -726,6 +890,7 @@ mod tests {
|
||||
|
||||
output "eDP-1" {
|
||||
scale 2.0
|
||||
transform "flipped-90"
|
||||
position x=10 y=20
|
||||
mode "1920x1080@144"
|
||||
}
|
||||
@@ -739,7 +904,6 @@ mod tests {
|
||||
|
||||
border {
|
||||
width 3
|
||||
active-color 0 100 200 255
|
||||
inactive-color 255 200 100 0
|
||||
}
|
||||
|
||||
@@ -778,17 +942,28 @@ mod tests {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
animations {
|
||||
slowdown 2.0
|
||||
|
||||
workspace-switch { off; }
|
||||
|
||||
horizontal-view-movement {
|
||||
duration-ms 100
|
||||
curve "ease-out-expo"
|
||||
}
|
||||
}
|
||||
|
||||
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;}
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+Shift+E { quit skip-confirmation=true; }
|
||||
}
|
||||
|
||||
debug {
|
||||
animation-slowdown 2.0
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
}
|
||||
"#,
|
||||
@@ -807,6 +982,7 @@ mod tests {
|
||||
touchpad: Touchpad {
|
||||
tap: true,
|
||||
dwt: true,
|
||||
dwtp: true,
|
||||
natural_scroll: false,
|
||||
accel_speed: 0.2,
|
||||
accel_profile: Some(AccelProfile::Flat),
|
||||
@@ -826,6 +1002,7 @@ mod tests {
|
||||
off: false,
|
||||
name: "eDP-1".to_owned(),
|
||||
scale: 2.,
|
||||
transform: Transform::Flipped90,
|
||||
position: Some(Position { x: 10, y: 20 }),
|
||||
mode: Some(Mode {
|
||||
width: 1920,
|
||||
@@ -850,13 +1027,13 @@ mod tests {
|
||||
a: 0,
|
||||
},
|
||||
},
|
||||
border: FocusRing {
|
||||
border: Border {
|
||||
off: false,
|
||||
width: 3,
|
||||
active_color: Color {
|
||||
r: 0,
|
||||
g: 100,
|
||||
b: 200,
|
||||
r: 255,
|
||||
g: 200,
|
||||
b: 127,
|
||||
a: 255,
|
||||
},
|
||||
inactive_color: Color {
|
||||
@@ -896,6 +1073,19 @@ mod tests {
|
||||
hotkey_overlay: HotkeyOverlay {
|
||||
skip_at_startup: true,
|
||||
},
|
||||
animations: Animations {
|
||||
slowdown: 2.,
|
||||
workspace_switch: Animation {
|
||||
off: true,
|
||||
..Animation::unfilled()
|
||||
},
|
||||
horizontal_view_movement: Animation {
|
||||
duration_ms: Some(100),
|
||||
curve: Some(AnimationCurve::EaseOutExpo),
|
||||
..Animation::unfilled()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
binds: Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
@@ -939,9 +1129,15 @@ mod tests {
|
||||
},
|
||||
actions: vec![Action::FocusWorkspace(1)],
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::e,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
|
||||
},
|
||||
actions: vec![Action::Quit(true)],
|
||||
},
|
||||
]),
|
||||
debug: DebugConfig {
|
||||
animation_slowdown: 2.,
|
||||
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -8,4 +8,8 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
|
||||
+260
-3
@@ -2,6 +2,7 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -9,21 +10,225 @@ use serde::{Deserialize, Serialize};
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
|
||||
/// Request from client to niri.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Request {
|
||||
/// Request information about connected outputs.
|
||||
Outputs,
|
||||
/// Perform an action.
|
||||
Action(Action),
|
||||
}
|
||||
|
||||
/// Response from niri to client.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
/// Reply from niri to client.
|
||||
///
|
||||
/// Every request gets one reply.
|
||||
///
|
||||
/// * If an error had occurred, it will be an `Reply::Err`.
|
||||
/// * If the request does not need any particular response, it will be
|
||||
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
|
||||
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
|
||||
pub type Reply = Result<Response, String>;
|
||||
|
||||
/// Successful response from niri to client.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Response {
|
||||
/// A request that does not need a response was handled successfully.
|
||||
Handled,
|
||||
/// Information about connected outputs.
|
||||
///
|
||||
/// Map from connector name to output info.
|
||||
Outputs(HashMap<String, Output>),
|
||||
}
|
||||
|
||||
/// Actions that niri can perform.
|
||||
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
|
||||
// variants from niri-config should be present here.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::Parser))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
|
||||
pub enum Action {
|
||||
/// Exit niri.
|
||||
Quit {
|
||||
/// Skip the "Press Enter to confirm" prompt.
|
||||
#[cfg_attr(feature = "clap", arg(short, long))]
|
||||
skip_confirmation: bool,
|
||||
},
|
||||
/// Power off all monitors via DPMS.
|
||||
PowerOffMonitors,
|
||||
/// Spawn a command.
|
||||
Spawn {
|
||||
/// Command to spawn.
|
||||
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
||||
command: Vec<String>,
|
||||
},
|
||||
/// Open the screenshot UI.
|
||||
Screenshot,
|
||||
/// Screenshot the focused screen.
|
||||
ScreenshotScreen,
|
||||
/// Screenshot the focused window.
|
||||
ScreenshotWindow,
|
||||
/// Close the focused window.
|
||||
CloseWindow,
|
||||
/// Toggle fullscreen on the focused window.
|
||||
FullscreenWindow,
|
||||
/// Focus the column to the left.
|
||||
FocusColumnLeft,
|
||||
/// Focus the column to the right.
|
||||
FocusColumnRight,
|
||||
/// Focus the first column.
|
||||
FocusColumnFirst,
|
||||
/// Focus the last column.
|
||||
FocusColumnLast,
|
||||
/// Focus the window below.
|
||||
FocusWindowDown,
|
||||
/// Focus the window above.
|
||||
FocusWindowUp,
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceDown,
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceUp,
|
||||
/// Move the focused column to the left.
|
||||
MoveColumnLeft,
|
||||
/// Move the focused column to the right.
|
||||
MoveColumnRight,
|
||||
/// Move the focused column to the start of the workspace.
|
||||
MoveColumnToFirst,
|
||||
/// Move the focused column to the end of the workspace.
|
||||
MoveColumnToLast,
|
||||
/// Move the focused window down in a column.
|
||||
MoveWindowDown,
|
||||
/// Move the focused window up in a column.
|
||||
MoveWindowUp,
|
||||
/// Move the focused window down in a column or to the workspace below.
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
/// Move the focused window up in a column or to the workspace above.
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
/// Consume or expel the focused window left.
|
||||
ConsumeOrExpelWindowLeft,
|
||||
/// Consume or expel the focused window right.
|
||||
ConsumeOrExpelWindowRight,
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn,
|
||||
/// Expel the focused window from the column.
|
||||
ExpelWindowFromColumn,
|
||||
/// Center the focused column on the screen.
|
||||
CenterColumn,
|
||||
/// Focus the workspace below.
|
||||
FocusWorkspaceDown,
|
||||
/// Focus the workspace above.
|
||||
FocusWorkspaceUp,
|
||||
/// Focus a workspace by index.
|
||||
FocusWorkspace {
|
||||
/// Index of the workspace to focus.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused window to the workspace below.
|
||||
MoveWindowToWorkspaceDown,
|
||||
/// Move the focused window to the workspace above.
|
||||
MoveWindowToWorkspaceUp,
|
||||
/// Move the focused window to a workspace by index.
|
||||
MoveWindowToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused column to the workspace below.
|
||||
MoveColumnToWorkspaceDown,
|
||||
/// Move the focused column to the workspace above.
|
||||
MoveColumnToWorkspaceUp,
|
||||
/// Move the focused column to a workspace by index.
|
||||
MoveColumnToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Move the focused workspace down.
|
||||
MoveWorkspaceDown,
|
||||
/// Move the focused workspace up.
|
||||
MoveWorkspaceUp,
|
||||
/// Focus the monitor to the left.
|
||||
FocusMonitorLeft,
|
||||
/// Focus the monitor to the right.
|
||||
FocusMonitorRight,
|
||||
/// Focus the monitor below.
|
||||
FocusMonitorDown,
|
||||
/// Focus the monitor above.
|
||||
FocusMonitorUp,
|
||||
/// Move the focused window to the monitor to the left.
|
||||
MoveWindowToMonitorLeft,
|
||||
/// Move the focused window to the monitor to the right.
|
||||
MoveWindowToMonitorRight,
|
||||
/// Move the focused window to the monitor below.
|
||||
MoveWindowToMonitorDown,
|
||||
/// Move the focused window to the monitor above.
|
||||
MoveWindowToMonitorUp,
|
||||
/// Move the focused column to the monitor to the left.
|
||||
MoveColumnToMonitorLeft,
|
||||
/// Move the focused column to the monitor to the right.
|
||||
MoveColumnToMonitorRight,
|
||||
/// Move the focused column to the monitor below.
|
||||
MoveColumnToMonitorDown,
|
||||
/// Move the focused column to the monitor above.
|
||||
MoveColumnToMonitorUp,
|
||||
/// Change the height of the focused window.
|
||||
SetWindowHeight {
|
||||
/// How to change the height.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Switch between preset column widths.
|
||||
SwitchPresetColumnWidth,
|
||||
/// Toggle the maximized state of the focused column.
|
||||
MaximizeColumn,
|
||||
/// Change the width of the focused column.
|
||||
SetColumnWidth {
|
||||
/// How to change the width.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Switch between keyboard layouts.
|
||||
SwitchLayout {
|
||||
/// Layout to switch to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
layout: LayoutSwitchTarget,
|
||||
},
|
||||
/// Show the hotkey overlay.
|
||||
ShowHotkeyOverlay,
|
||||
/// Move the focused workspace to the monitor to the left.
|
||||
MoveWorkspaceToMonitorLeft,
|
||||
/// Move the focused workspace to the monitor to the right.
|
||||
MoveWorkspaceToMonitorRight,
|
||||
/// Move the focused workspace to the monitor below.
|
||||
MoveWorkspaceToMonitorDown,
|
||||
/// Move the focused workspace to the monitor above.
|
||||
MoveWorkspaceToMonitorUp,
|
||||
/// Toggle a debug tint on windows.
|
||||
ToggleDebugTint,
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SizeChange {
|
||||
/// Set the size in logical pixels.
|
||||
SetFixed(i32),
|
||||
/// Set the size as a proportion of the working area.
|
||||
SetProportion(f64),
|
||||
/// Add or subtract to the current size in logical pixels.
|
||||
AdjustFixed(i32),
|
||||
/// Add or subtract to the current size as a proportion of the working area.
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
/// Layout to switch to.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LayoutSwitchTarget {
|
||||
/// The next configured layout.
|
||||
Next,
|
||||
/// The previous configured layout.
|
||||
Prev,
|
||||
}
|
||||
|
||||
/// Connected output.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Output {
|
||||
@@ -53,3 +258,55 @@ pub struct Mode {
|
||||
/// Refresh rate in millihertz.
|
||||
pub refresh_rate: u32,
|
||||
}
|
||||
|
||||
impl FromStr for SizeChange {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once('%') {
|
||||
Some((value, empty)) => {
|
||||
if !empty.is_empty() {
|
||||
return Err("trailing characters after '%' are not allowed");
|
||||
}
|
||||
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::AdjustProportion(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::SetProportion(value))
|
||||
}
|
||||
None => Err("value is missing"),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let value = s;
|
||||
match value.bytes().next() {
|
||||
Some(b'-' | b'+') => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::AdjustFixed(value))
|
||||
}
|
||||
Some(_) => {
|
||||
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||
Ok(Self::SetFixed(value))
|
||||
}
|
||||
None => Err("value is missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LayoutSwitchTarget {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"next" => Ok(Self::Next),
|
||||
"prev" => Ok(Self::Prev),
|
||||
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "niri-visual-tests"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.8.0", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.1", path = ".." }
|
||||
niri-config = { version = "0.1.1", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,14 @@
|
||||
# niri-visual-tests
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a development-only app, you shouldn't package it.
|
||||
|
||||
This app contains a number of hard-coded test scenarios for visual inspection.
|
||||
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
|
||||
The idea is to go through the test scenarios and check that everything *looks* right.
|
||||
|
||||
## Running
|
||||
|
||||
You will need recent GTK and libadwaita.
|
||||
Then, `cargo run`.
|
||||
@@ -0,0 +1,3 @@
|
||||
.anim-control-bar {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::workspace::ColumnWidth;
|
||||
use niri::layout::Options;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
|
||||
pub struct Layout {
|
||||
output: Output,
|
||||
windows: Vec<TestWindow>,
|
||||
layout: niri::layout::Layout<TestWindow>,
|
||||
start_time: Duration,
|
||||
steps: HashMap<Duration, DynStepFn>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(size: Size<i32, Logical>) -> Self {
|
||||
let output = Output::new(
|
||||
String::new(),
|
||||
PhysicalProperties {
|
||||
size: Size::from((size.w, size.h)),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
},
|
||||
);
|
||||
let mode = Some(Mode {
|
||||
size: size.to_physical(1),
|
||||
refresh: 60000,
|
||||
});
|
||||
output.change_current_state(mode, None, None, None);
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 4,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
inactive_color: Color::new(50, 50, 50, 255),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = niri::layout::Layout::with_options(options);
|
||||
layout.add_output(output.clone());
|
||||
|
||||
Self {
|
||||
output,
|
||||
windows: Vec::new(),
|
||||
layout,
|
||||
start_time: get_monotonic_time(),
|
||||
steps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.layout.activate_window(&rv.windows[0]);
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(&win);
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(&window);
|
||||
}
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
window: TestWindow,
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
self.layout
|
||||
.add_window_right_of(right_of, window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(&window);
|
||||
}
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
|
||||
self.steps
|
||||
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Layout {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let mode = Some(Mode {
|
||||
size: Size::from((width, height)),
|
||||
refresh: 60000,
|
||||
});
|
||||
self.output.change_current_state(mode, None, None, None);
|
||||
layer_map_for_output(&self.output).arrange();
|
||||
self.layout.update_output_size(&self.output);
|
||||
for win in &self.windows {
|
||||
if win.communicate() {
|
||||
self.layout.update_window(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.are_animations_ongoing()
|
||||
|| !self.steps.is_empty()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, mut current_time: Duration) {
|
||||
let run = self
|
||||
.steps
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|delay| self.start_time + *delay <= current_time)
|
||||
.collect::<Vec<_>>();
|
||||
for key in &run {
|
||||
let f = self.steps.remove(key).unwrap();
|
||||
f(self);
|
||||
}
|
||||
if !run.is_empty() {
|
||||
current_time = get_monotonic_time();
|
||||
}
|
||||
|
||||
self.layout.advance_animations(current_time);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
pub trait TestCase {
|
||||
fn resize(&mut self, width: i32, height: i32);
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn advance_animations(&mut self, _current_time: Duration) {}
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Tile {
|
||||
window: TestWindow,
|
||||
tile: niri::layout::tile::Tile<TestWindow>,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::freeform(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn with_window(window: TestWindow) -> Self {
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
..Default::default()
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 32,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
|
||||
Self { window, tile }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.tile.request_tile_size(Size::from((width, height)));
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.tile.are_animations_ongoing()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.tile.advance_animations(current_time, true);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let tile_size = self.tile.tile_size().to_physical(1);
|
||||
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
|
||||
|
||||
self.tile
|
||||
.render(renderer, location, Scale::from(1.), true)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use niri::layout::LayoutElement;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Window {
|
||||
window: TestWindow,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(size);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window.request_size(Size::from((width, height)));
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let win_size = self.window.size().to_physical(1);
|
||||
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
|
||||
|
||||
self.window
|
||||
.render(renderer, location, Scale::from(1.))
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::env;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||
use cases::tile::Tile;
|
||||
use cases::window::Window;
|
||||
use gtk::prelude::{
|
||||
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
||||
};
|
||||
use gtk::{gdk, gio, glib};
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay_view::SmithayView;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cases::layout::Layout;
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod cases;
|
||||
mod smithay_view;
|
||||
mod test_window;
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
let directives =
|
||||
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
|
||||
app.connect_startup(on_startup);
|
||||
app.connect_activate(build_ui);
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn on_startup(_app: &adw::Application) {
|
||||
// Load our CSS.
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_string(include_str!("../resources/style.css"));
|
||||
if let Some(display) = gdk::Display::default() {
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ui(app: &adw::Application) {
|
||||
let stack = gtk::Stack::new();
|
||||
|
||||
struct S {
|
||||
stack: gtk::Stack,
|
||||
}
|
||||
|
||||
impl S {
|
||||
fn add<T: TestCase + 'static>(
|
||||
&self,
|
||||
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
title: &str,
|
||||
) {
|
||||
let view = SmithayView::new(make);
|
||||
self.stack.add_titled(&view, None, title);
|
||||
}
|
||||
}
|
||||
|
||||
let s = S {
|
||||
stack: stack.clone(),
|
||||
};
|
||||
|
||||
s.add(Window::freeform, "Freeform Window");
|
||||
s.add(Window::fixed_size, "Fixed Size Window");
|
||||
s.add(
|
||||
Window::fixed_size_with_csd_shadow,
|
||||
"Fixed Size Window - CSD Shadow",
|
||||
);
|
||||
|
||||
s.add(Tile::freeform, "Freeform Tile");
|
||||
s.add(Tile::fixed_size, "Fixed Size Tile");
|
||||
s.add(
|
||||
Tile::fixed_size_with_csd_shadow,
|
||||
"Fixed Size Tile - CSD Shadow",
|
||||
);
|
||||
s.add(Tile::freeform_open, "Freeform Tile - Open");
|
||||
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
|
||||
s.add(
|
||||
Tile::fixed_size_with_csd_shadow_open,
|
||||
"Fixed Size Tile - CSD Shadow - Open",
|
||||
);
|
||||
|
||||
s.add(Layout::open_in_between, "Layout - Open In-Between");
|
||||
s.add(
|
||||
Layout::open_multiple_quickly,
|
||||
"Layout - Open Multiple Quickly",
|
||||
);
|
||||
s.add(
|
||||
Layout::open_multiple_quickly_big,
|
||||
"Layout - Open Multiple Quickly - Big",
|
||||
);
|
||||
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
|
||||
s.add(
|
||||
Layout::open_to_the_left_big,
|
||||
"Layout - Open To The Left - Big",
|
||||
);
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
anim_adjustment
|
||||
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
|
||||
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||
anim_scale.set_hexpand(true);
|
||||
|
||||
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
anim_control_bar.add_css_class("anim-control-bar");
|
||||
anim_control_bar.append(>k::Label::new(Some("Slowdown")));
|
||||
anim_control_bar.append(&anim_scale);
|
||||
|
||||
let content_view = adw::ToolbarView::new();
|
||||
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||
content_view.add_top_bar(&content_headerbar);
|
||||
content_view.add_bottom_bar(&anim_control_bar);
|
||||
content_view.set_content(Some(&stack));
|
||||
let content = adw::NavigationPage::new(
|
||||
&content_view,
|
||||
stack
|
||||
.page(&stack.visible_child().unwrap())
|
||||
.title()
|
||||
.as_deref()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let sidebar_header = adw::HeaderBar::new();
|
||||
let stack_sidebar = gtk::StackSidebar::new();
|
||||
stack_sidebar.set_stack(&stack);
|
||||
let sidebar_view = adw::ToolbarView::new();
|
||||
sidebar_view.add_top_bar(&sidebar_header);
|
||||
sidebar_view.set_content(Some(&stack_sidebar));
|
||||
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
|
||||
|
||||
let split_view = adw::NavigationSplitView::new();
|
||||
split_view.set_content(Some(&content));
|
||||
split_view.set_sidebar(Some(&sidebar));
|
||||
|
||||
stack.connect_visible_child_notify(move |stack| {
|
||||
content.set_title(
|
||||
stack
|
||||
.visible_child()
|
||||
.and_then(|c| stack.page(&c).title())
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = adw::ApplicationWindow::new(app);
|
||||
window.set_title(Some("niri visual tests"));
|
||||
window.set_content(Some(&split_view));
|
||||
window.present();
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
use gtk::glib;
|
||||
use gtk::subclass::prelude::*;
|
||||
use smithay::utils::{Logical, Size};
|
||||
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::ptr::null;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||
use smithay::backend::renderer::{Frame, Renderer, Unbind};
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
gl_area: gtk::GLArea,
|
||||
size: Cell<(i32, i32)>,
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for SmithayView {
|
||||
const NAME: &'static str = "NiriSmithayView";
|
||||
type Type = super::SmithayView;
|
||||
type ParentType = gtk::Widget;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for SmithayView {
|
||||
fn constructed(&self) {
|
||||
let obj = self.obj();
|
||||
|
||||
self.parent_constructed();
|
||||
|
||||
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
|
||||
self.gl_area.set_parent(&*obj);
|
||||
|
||||
self.gl_area.connect_resize({
|
||||
let imp = self.downgrade();
|
||||
move |_, width, height| {
|
||||
if let Some(imp) = imp.upgrade() {
|
||||
imp.resize(width, height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.gl_area.connect_render({
|
||||
let imp = self.downgrade();
|
||||
move |_, gl_context| {
|
||||
if let Some(imp) = imp.upgrade() {
|
||||
if let Err(err) = imp.render(gl_context) {
|
||||
warn!("error rendering: {err:?}");
|
||||
}
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
});
|
||||
|
||||
obj.add_tick_callback(|obj, _frame_clock| {
|
||||
let imp = obj.imp();
|
||||
|
||||
if let Some(case) = &mut *imp.test_case.borrow_mut() {
|
||||
if case.are_animations_ongoing() {
|
||||
imp.gl_area.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.gl_area.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for SmithayView {
|
||||
fn unmap(&self) {
|
||||
self.test_case.replace(None);
|
||||
self.parent_unmap();
|
||||
}
|
||||
|
||||
fn unrealize(&self) {
|
||||
self.renderer.replace(None);
|
||||
self.parent_unrealize();
|
||||
}
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
fn resize(&self, width: i32, height: i32) {
|
||||
self.size.set((width, height));
|
||||
|
||||
if let Some(case) = &mut *self.test_case.borrow_mut() {
|
||||
case.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
|
||||
// Set up the Smithay renderer.
|
||||
let mut renderer = self.renderer.borrow_mut();
|
||||
let renderer = renderer.get_or_insert_with(|| {
|
||||
unsafe { create_renderer() }
|
||||
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
|
||||
});
|
||||
let Ok(renderer) = renderer else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
// Create the test case if missing.
|
||||
let mut case = self.test_case.borrow_mut();
|
||||
let case = case.get_or_insert_with(|| {
|
||||
let make = self.make_test_case.get().unwrap();
|
||||
make(Size::from(size))
|
||||
});
|
||||
|
||||
case.advance_animations(get_monotonic_time());
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
|
||||
|
||||
let elements = unsafe {
|
||||
with_framebuffer_save_restore(renderer, |renderer| {
|
||||
case.render(renderer, Size::from(size))
|
||||
})
|
||||
}?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(rect.size, Transform::Normal)
|
||||
.context("error creating frame")?;
|
||||
|
||||
frame
|
||||
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements.iter().rev() {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(Scale::from(1.));
|
||||
|
||||
if let Some(mut damage) = rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
||||
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||
.context("error loading EGL symbols in Smithay")?;
|
||||
|
||||
let egl_display = egl::GetCurrentDisplay();
|
||||
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
|
||||
|
||||
let egl_context = egl::GetCurrentContext();
|
||||
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
|
||||
|
||||
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
|
||||
// really use it anyway so just get some random one.
|
||||
let mut egl_config_id = null();
|
||||
let mut num_configs = 0;
|
||||
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
|
||||
ensure!(res == egl::TRUE, "error choosing EGL config");
|
||||
ensure!(num_configs != 0, "no EGL config");
|
||||
|
||||
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
|
||||
.context("error creating EGL context")?;
|
||||
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
|
||||
.context("error getting supported renderer capabilities")?
|
||||
.into_iter()
|
||||
.filter(|c| *c != Capability::ColorTransformations);
|
||||
|
||||
GlesRenderer::with_capabilities(egl_context, capabilities)
|
||||
.context("error creating GlesRenderer")
|
||||
}
|
||||
|
||||
unsafe fn with_framebuffer_save_restore<T>(
|
||||
renderer: &mut GlesRenderer,
|
||||
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
let rv = f(renderer);
|
||||
|
||||
renderer.unbind().context("error unbinding")?;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||
@extends gtk::Widget;
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
pub fn new<T: TestCase + 'static>(
|
||||
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder().build();
|
||||
|
||||
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
|
||||
let make_test_case = Box::new(make) as _;
|
||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||
|
||||
obj
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{LayoutElement, LayoutElementRenderElement};
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Size, Transform};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestWindowInner {
|
||||
id: usize,
|
||||
size: Size<i32, Logical>,
|
||||
requested_size: Option<Size<i32, Logical>>,
|
||||
min_size: Size<i32, Logical>,
|
||||
max_size: Size<i32, Logical>,
|
||||
buffer: SolidColorBuffer,
|
||||
pending_fullscreen: bool,
|
||||
csd_shadow_width: i32,
|
||||
csd_shadow_buffer: SolidColorBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
|
||||
|
||||
impl TestWindow {
|
||||
pub fn freeform(id: usize) -> Self {
|
||||
let size = Size::from((100, 200));
|
||||
let min_size = Size::from((0, 0));
|
||||
let max_size = Size::from((0, 0));
|
||||
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
|
||||
|
||||
Self(Rc::new(RefCell::new(TestWindowInner {
|
||||
id,
|
||||
size,
|
||||
requested_size: None,
|
||||
min_size,
|
||||
max_size,
|
||||
buffer,
|
||||
pending_fullscreen: false,
|
||||
csd_shadow_width: 0,
|
||||
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn fixed_size(id: usize) -> Self {
|
||||
let rv = Self::freeform(id);
|
||||
rv.set_min_size((200, 400).into());
|
||||
rv.set_max_size((200, 400).into());
|
||||
rv.set_color([0.88, 0.11, 0.14, 1.]);
|
||||
rv.communicate();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn set_min_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().min_size = size;
|
||||
}
|
||||
|
||||
pub fn set_max_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().max_size = size;
|
||||
}
|
||||
|
||||
pub fn set_color(&self, color: [f32; 4]) {
|
||||
self.0.borrow_mut().buffer.set_color(color);
|
||||
}
|
||||
|
||||
pub fn set_csd_shadow_width(&self, width: i32) {
|
||||
self.0.borrow_mut().csd_shadow_width = width;
|
||||
}
|
||||
|
||||
pub fn communicate(&self) -> bool {
|
||||
let mut rv = false;
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
let mut new_size = inner.size;
|
||||
|
||||
if let Some(size) = inner.requested_size.take() {
|
||||
assert!(size.w >= 0);
|
||||
assert!(size.h >= 0);
|
||||
|
||||
if size.w != 0 {
|
||||
new_size.w = size.w;
|
||||
}
|
||||
if size.h != 0 {
|
||||
new_size.h = size.h;
|
||||
}
|
||||
}
|
||||
|
||||
if inner.max_size.w > 0 {
|
||||
new_size.w = min(new_size.w, inner.max_size.w);
|
||||
}
|
||||
if inner.max_size.h > 0 {
|
||||
new_size.h = min(new_size.h, inner.max_size.h);
|
||||
}
|
||||
if inner.min_size.w > 0 {
|
||||
new_size.w = max(new_size.w, inner.min_size.w);
|
||||
}
|
||||
if inner.min_size.h > 0 {
|
||||
new_size.h = max(new_size.h, inner.min_size.h);
|
||||
}
|
||||
|
||||
if inner.size != new_size {
|
||||
inner.size = new_size;
|
||||
inner.buffer.resize(new_size);
|
||||
rv = true;
|
||||
}
|
||||
|
||||
let mut csd_shadow_size = new_size;
|
||||
csd_shadow_size.w += inner.csd_shadow_width * 2;
|
||||
csd_shadow_size.h += inner.csd_shadow_width * 2;
|
||||
inner.csd_shadow_buffer.resize(csd_shadow_size);
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TestWindow {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.borrow().id == other.0.borrow().id
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutElement for TestWindow {
|
||||
fn size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().size
|
||||
}
|
||||
|
||||
fn buf_loc(&self) -> Point<i32, Logical> {
|
||||
(0, 0).into()
|
||||
}
|
||||
|
||||
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
let inner = self.0.borrow();
|
||||
|
||||
vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
|
||||
.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().requested_size = Some(size);
|
||||
self.0.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||
self.0.borrow_mut().pending_fullscreen = true;
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().min_size
|
||||
}
|
||||
|
||||
fn max_size(&self) -> Size<i32, Logical> {
|
||||
self.0.borrow().max_size
|
||||
}
|
||||
|
||||
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
|
||||
|
||||
fn has_ssd(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn output_enter(&self, _output: &Output) {}
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
self.0.borrow().pending_fullscreen
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ input {
|
||||
touchpad {
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
@@ -65,6 +66,10 @@ input {
|
||||
// Scale is a floating-point number, but at the moment only integer values work.
|
||||
scale 2.0
|
||||
|
||||
// Transform allows to rotate the output counter-clockwise, valid values are:
|
||||
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
|
||||
transform "normal"
|
||||
|
||||
// Resolution and, optionally, refresh rate of the output.
|
||||
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
|
||||
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
||||
@@ -86,6 +91,14 @@ input {
|
||||
}
|
||||
|
||||
layout {
|
||||
// By default focus ring and border are rendered as a solid background rectangle
|
||||
// behind windows. That is, they will show up through semitransparent windows.
|
||||
// This is because windows using client-side decoratins can have an arbitrary shape.
|
||||
//
|
||||
// If you don't like that, you should uncomment `prefer-no-csd` below.
|
||||
// Niri will draw focus ring and border *around* windows that agree to omit their
|
||||
// client-side decorations.
|
||||
|
||||
// You can change how the focus ring looks.
|
||||
focus-ring {
|
||||
// Uncomment this line to disable the focus ring.
|
||||
@@ -117,9 +130,9 @@ layout {
|
||||
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
||||
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||
proportion 0.333
|
||||
proportion 0.33333
|
||||
proportion 0.5
|
||||
proportion 0.667
|
||||
proportion 0.66667
|
||||
|
||||
// Fixed sets the width in logical pixels exactly.
|
||||
// fixed 1920
|
||||
@@ -185,6 +198,56 @@ hotkey-overlay {
|
||||
// skip-at-startup
|
||||
}
|
||||
|
||||
// Animation settings.
|
||||
animations {
|
||||
// Uncomment to turn off all animations.
|
||||
// off
|
||||
|
||||
// Slow down all animations by this factor. Values below 1 speed them up instead.
|
||||
// slowdown 3.0
|
||||
|
||||
// You can configure all individual animations.
|
||||
// Available settings are the same for all of them:
|
||||
// - off disables the animation.
|
||||
// - duration-ms sets the duration of the animation in milliseconds.
|
||||
// - curve sets the easing curve. Currently, available curves
|
||||
// are "ease-out-cubic" and "ease-out-expo".
|
||||
|
||||
// Animation when switching workspaces up and down,
|
||||
// including after the touchpad gesture.
|
||||
workspace-switch {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
}
|
||||
|
||||
// All horizontal camera view movement:
|
||||
// - When a window off-screen is focused and the camera scrolls to it.
|
||||
// - When a new window appears off-screen and the camera scrolls to it.
|
||||
// - When a window resizes bigger and the camera scrolls to show it in full.
|
||||
// - And so on.
|
||||
horizontal-view-movement {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
}
|
||||
|
||||
// Window opening animation. Note that this one has different defaults.
|
||||
window-open {
|
||||
// off
|
||||
// duration-ms 150
|
||||
// curve "ease-out-expo"
|
||||
}
|
||||
|
||||
// Config parse error and new default config creation notification
|
||||
// open/close animation.
|
||||
config-notification-open-close {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
}
|
||||
}
|
||||
|
||||
binds {
|
||||
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||
// in the end. To find an XKB name for a particular key, you may use a program
|
||||
@@ -192,6 +255,9 @@ binds {
|
||||
//
|
||||
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
||||
// when running as a winit window.
|
||||
//
|
||||
// Most actions that you can bind here can also be invoked programmatically with
|
||||
// `niri msg action do-something`.
|
||||
|
||||
// Mod-Shift-/, which is usually the same as Mod-?,
|
||||
// shows a list of important hotkeys.
|
||||
@@ -200,7 +266,7 @@ binds {
|
||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||
Mod+T { spawn "alacritty"; }
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Mod+Alt+L { spawn "swaylock"; }
|
||||
Super+Alt+L { spawn "swaylock"; }
|
||||
|
||||
// You can also use a shell:
|
||||
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||
@@ -263,6 +329,10 @@ binds {
|
||||
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||
// ...
|
||||
|
||||
// And you can also move a whole workspace to another monitor:
|
||||
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
|
||||
// ...
|
||||
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+U { focus-workspace-down; }
|
||||
@@ -306,6 +376,10 @@ binds {
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// There are also commands that consume or expel a single window to the side.
|
||||
// Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
// Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
@@ -338,7 +412,11 @@ binds {
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
// If you want to skip the confirmation dialog, set the flag like so:
|
||||
// Mod+Shift+E { quit skip-confirmation=true; }
|
||||
Mod+Shift+E { quit; }
|
||||
|
||||
Mod+Shift+P { power-off-monitors; }
|
||||
|
||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||
@@ -364,9 +442,6 @@ debug {
|
||||
// The cursor will be rendered together with the rest of the frame.
|
||||
// disable-cursor-plane
|
||||
|
||||
// Slow down animations by this factor.
|
||||
// animation-slowdown 3.0
|
||||
|
||||
// Override the DRM device that niri will use for all rendering.
|
||||
// render-drm-device "/dev/dri/renderD129"
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@ systemctl --user --wait start niri.service
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
|
||||
+44
-3
@@ -15,20 +15,43 @@ pub struct Animation {
|
||||
duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
curve: Curve,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Curve {
|
||||
EaseOutCubic,
|
||||
EaseOutExpo,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(from: f64, to: f64, over: Duration) -> Self {
|
||||
pub fn new(
|
||||
from: f64,
|
||||
to: f64,
|
||||
config: niri_config::Animation,
|
||||
default: niri_config::Animation,
|
||||
) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration_ms = if config.off {
|
||||
0
|
||||
} else {
|
||||
config.duration_ms.unwrap_or(default.duration_ms.unwrap())
|
||||
};
|
||||
let duration = Duration::from_millis(u64::from(duration_ms))
|
||||
.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed));
|
||||
|
||||
let curve = Curve::from(config.curve.unwrap_or(default.curve.unwrap()));
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
curve,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +67,7 @@ impl Animation {
|
||||
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
|
||||
self.curve.y(x) * (self.to - self.from) + self.from
|
||||
}
|
||||
|
||||
pub fn to(&self) -> f64 {
|
||||
@@ -56,3 +79,21 @@ impl Animation {
|
||||
self.from
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub fn y(self, x: f64) -> f64 {
|
||||
match self {
|
||||
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<niri_config::AnimationCurve> for Curve {
|
||||
fn from(value: niri_config::AnimationCurve) -> Self {
|
||||
match value {
|
||||
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
||||
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::Niri;
|
||||
use crate::niri::Niri;
|
||||
|
||||
pub mod tty;
|
||||
pub use tty::Tty;
|
||||
@@ -98,7 +98,7 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||
@@ -138,7 +138,7 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_monitors_active(&self, active: bool) {
|
||||
pub fn set_monitors_active(&mut self, active: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||
Backend::Winit(_) => (),
|
||||
|
||||
+191
-94
@@ -20,7 +20,7 @@ use smithay::backend::drm::{
|
||||
use smithay::backend::egl::context::ContextPriority;
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
||||
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
@@ -41,6 +41,9 @@ use smithay::reexports::wayland_protocols;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::DeviceFd;
|
||||
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
||||
use smithay_drm_extras::edid::EdidInfo;
|
||||
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
||||
@@ -48,10 +51,9 @@ use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
|
||||
use super::RenderResult;
|
||||
use crate::frame_clock::FrameClock;
|
||||
use crate::niri::{RedrawState, State};
|
||||
use crate::render_helpers::AsGlesRenderer;
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::Niri;
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
|
||||
|
||||
@@ -74,6 +76,8 @@ pub struct Tty {
|
||||
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
|
||||
// the primary GPU.
|
||||
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
|
||||
// The output config had changed, but the session is paused, so we need to update it on resume.
|
||||
update_output_config_on_resume: bool,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
@@ -104,7 +108,7 @@ type GbmDrmCompositor = DrmCompositor<
|
||||
DrmDeviceFd,
|
||||
>;
|
||||
|
||||
struct OutputDevice {
|
||||
pub struct OutputDevice {
|
||||
token: RegistrationToken,
|
||||
render_node: DrmNode,
|
||||
drm_scanner: DrmScanner,
|
||||
@@ -113,6 +117,42 @@ struct OutputDevice {
|
||||
// See https://github.com/Smithay/smithay/issues/1102.
|
||||
drm: DrmDevice,
|
||||
gbm: GbmDevice<DrmDeviceFd>,
|
||||
|
||||
pub drm_lease_state: DrmLeaseState,
|
||||
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
|
||||
active_leases: Vec<DrmLease>,
|
||||
}
|
||||
|
||||
impl OutputDevice {
|
||||
pub fn lease_request(
|
||||
&self,
|
||||
request: DrmLeaseRequest,
|
||||
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||
let mut builder = DrmLeaseBuilder::new(&self.drm);
|
||||
for connector in request.connectors {
|
||||
let (_, crtc) = self
|
||||
.non_desktop_connectors
|
||||
.iter()
|
||||
.find(|(conn, _)| connector == *conn)
|
||||
.ok_or_else(|| {
|
||||
warn!("Attempted to lease connector that is not non-desktop");
|
||||
LeaseRejected::default()
|
||||
})?;
|
||||
builder.add_connector(connector);
|
||||
builder.add_crtc(*crtc);
|
||||
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
|
||||
builder.add_plane(planes.primary.handle);
|
||||
}
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn new_lease(&mut self, lease: DrmLease) {
|
||||
self.active_leases.push(lease);
|
||||
}
|
||||
|
||||
pub fn remove_lease(&mut self, lease_id: u32) {
|
||||
self.active_leases.retain(|l| l.id() != lease_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -142,11 +182,19 @@ pub struct SurfaceDmabufFeedback {
|
||||
}
|
||||
|
||||
impl Tty {
|
||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
|
||||
let (session, notifier) = LibSeatSession::new().unwrap();
|
||||
pub fn new(
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (session, notifier) = LibSeatSession::new().context(
|
||||
"Error creating a session. This might mean that you're trying to run niri on a TTY \
|
||||
that is already busy, for example if you're running this inside tmux that had been \
|
||||
originally started on a different TTY",
|
||||
)?;
|
||||
let seat_name = session.seat();
|
||||
|
||||
let udev_backend = UdevBackend::new(session.seat()).unwrap();
|
||||
let udev_backend =
|
||||
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
|
||||
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
|
||||
state.backend.tty().on_udev_event(&mut state.niri, event);
|
||||
});
|
||||
@@ -155,7 +203,9 @@ impl Tty {
|
||||
.unwrap();
|
||||
|
||||
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||
libinput.udev_assign_seat(&seat_name).unwrap();
|
||||
libinput
|
||||
.udev_assign_seat(&seat_name)
|
||||
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
||||
|
||||
let input_backend = LibinputInputBackend::new(libinput.clone());
|
||||
event_loop
|
||||
@@ -190,18 +240,23 @@ impl Tty {
|
||||
Ok(gles)
|
||||
};
|
||||
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
|
||||
let gpu_manager = GpuManager::new(api).unwrap();
|
||||
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
|
||||
|
||||
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
|
||||
.unwrap_or_else(|| {
|
||||
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
|
||||
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
|
||||
.ok_or(())
|
||||
.or_else(|()| {
|
||||
let primary_gpu_path = udev::primary_gpu(&seat_name)
|
||||
.context("error getting the primary GPU")?
|
||||
.context("couldn't find a GPU")?;
|
||||
let primary_node = DrmNode::from_path(primary_gpu_path)
|
||||
.context("error opening the primary GPU DRM node")?;
|
||||
let primary_render_node = primary_node
|
||||
.node_with_type(NodeType::Render)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(primary_node, primary_render_node)
|
||||
});
|
||||
.context("error getting the render node for the primary GPU")?
|
||||
.context("error getting the render node for the primary GPU")?;
|
||||
|
||||
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
|
||||
})?;
|
||||
|
||||
let mut node_path = String::new();
|
||||
if let Some(path) = primary_render_node.dev_path() {
|
||||
@@ -211,7 +266,7 @@ impl Tty {
|
||||
}
|
||||
info!("using as the render node: {}", node_path);
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
config,
|
||||
session,
|
||||
udev_dispatcher,
|
||||
@@ -222,9 +277,10 @@ impl Tty {
|
||||
devices: HashMap::new(),
|
||||
dmabuf_global: None,
|
||||
primary_allocator: None,
|
||||
update_output_config_on_resume: false,
|
||||
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
|
||||
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
@@ -277,7 +333,7 @@ impl Tty {
|
||||
|
||||
self.libinput.suspend();
|
||||
|
||||
for device in self.devices.values() {
|
||||
for device in self.devices.values_mut() {
|
||||
device.drm.pause();
|
||||
}
|
||||
}
|
||||
@@ -320,47 +376,13 @@ impl Tty {
|
||||
device_list.remove(&node.dev_id());
|
||||
|
||||
// It hasn't been removed, update its state as usual.
|
||||
let device = &self.devices[&node];
|
||||
device.drm.activate();
|
||||
|
||||
// HACK: force reset the connectors to make resuming work across sleep.
|
||||
let device = &self.devices[&node];
|
||||
let crtcs: Vec<_> = device
|
||||
.drm_scanner
|
||||
.crtcs()
|
||||
.map(|(_conn, crtc)| crtc)
|
||||
.collect();
|
||||
for crtc in crtcs {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
}
|
||||
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
let _ = device.drm_scanner.scan_connectors(&device.drm);
|
||||
let crtcs: Vec<_> = device
|
||||
.drm_scanner
|
||||
.crtcs()
|
||||
.map(|(conn, crtc)| (conn.clone(), crtc))
|
||||
.collect();
|
||||
for (conn, crtc) in crtcs {
|
||||
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
if let Err(err) = device.drm.activate(true) {
|
||||
warn!("error activating DRM device: {err:?}");
|
||||
}
|
||||
|
||||
// // Refresh the connectors.
|
||||
// self.device_changed(node.dev_id(), niri);
|
||||
|
||||
// // Refresh the state on unchanged connectors.
|
||||
// let device = self.devices.get_mut(&node).unwrap();
|
||||
// for surface in device.surfaces.values_mut() {
|
||||
// let compositor = &mut surface.compositor;
|
||||
// if let Err(err) = compositor.surface().reset_state() {
|
||||
// warn!("error resetting DRM surface state: {err}");
|
||||
// }
|
||||
// compositor.reset_buffers();
|
||||
// }
|
||||
|
||||
// niri.queue_redraw_all();
|
||||
// Refresh the connectors.
|
||||
self.device_changed(node.dev_id(), niri);
|
||||
}
|
||||
|
||||
// Add new devices.
|
||||
@@ -370,7 +392,16 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
if self.update_output_config_on_resume {
|
||||
self.on_output_config_changed(niri);
|
||||
}
|
||||
|
||||
self.refresh_ipc_outputs();
|
||||
|
||||
niri.idle_notifier_state.notify_activity(&niri.seat);
|
||||
niri.monitors_active = true;
|
||||
self.set_monitors_active(true);
|
||||
niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,6 +415,8 @@ impl Tty {
|
||||
debug!("device added: {device_id} {path:?}");
|
||||
|
||||
let node = DrmNode::from_dev_id(device_id)?;
|
||||
let drm_lease_state = DrmLeaseState::new::<State>(&niri.display_handle, &node)
|
||||
.context("Couldn't create DrmLeaseState")?;
|
||||
|
||||
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK;
|
||||
let fd = self.session.open(path, open_flags)?;
|
||||
@@ -392,15 +425,9 @@ impl Tty {
|
||||
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
|
||||
let gbm = GbmDevice::new(device_fd)?;
|
||||
|
||||
let display = EGLDisplay::new(gbm.clone())?;
|
||||
let display = unsafe { EGLDisplay::new(gbm.clone())? };
|
||||
let egl_device = EGLDevice::device_for_display(&display)?;
|
||||
|
||||
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
|
||||
// same as the one we just created here, so when ours is dropped at the end of the scope,
|
||||
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
|
||||
// beforehand.
|
||||
drop(display);
|
||||
|
||||
let render_node = egl_device
|
||||
.try_get_render_node()?
|
||||
.context("no render node")?;
|
||||
@@ -479,6 +506,9 @@ impl Tty {
|
||||
gbm,
|
||||
drm_scanner: DrmScanner::new(),
|
||||
surfaces: HashMap::new(),
|
||||
drm_lease_state,
|
||||
active_leases: Vec::new(),
|
||||
non_desktop_connectors: HashSet::new(),
|
||||
};
|
||||
assert!(self.devices.insert(node, device).is_none());
|
||||
|
||||
@@ -600,6 +630,41 @@ impl Tty {
|
||||
);
|
||||
debug!("connecting connector: {output_name}");
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
let non_desktop = device
|
||||
.drm
|
||||
.get_properties(connector.handle())
|
||||
.ok()
|
||||
.and_then(|props| {
|
||||
let (info, value) = props
|
||||
.into_iter()
|
||||
.filter_map(|(handle, value)| {
|
||||
let info = device.drm.get_property(handle).ok()?;
|
||||
Some((info, value))
|
||||
})
|
||||
.find(|(info, _)| info.name().to_str() == Ok("non-desktop"))?;
|
||||
|
||||
info.value_type().convert_value(value).as_boolean()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if non_desktop {
|
||||
debug!("output is non desktop");
|
||||
let description = EdidInfo::for_connector(&device.drm, connector.handle())
|
||||
.map(|info| info.model)
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
device.drm_lease_state.add_connector::<State>(
|
||||
connector.handle(),
|
||||
output_name,
|
||||
description,
|
||||
);
|
||||
device
|
||||
.non_desktop_connectors
|
||||
.insert((connector.handle(), crtc));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -614,8 +679,6 @@ impl Tty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
for m in connector.modes() {
|
||||
trace!("{m:?}");
|
||||
}
|
||||
@@ -735,13 +798,8 @@ impl Tty {
|
||||
let sequence_delta_plot_name =
|
||||
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
|
||||
|
||||
self.enabled_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(output_name.clone(), output.clone());
|
||||
|
||||
let surface = Surface {
|
||||
name: output_name,
|
||||
name: output_name.clone(),
|
||||
compositor,
|
||||
dmabuf_feedback,
|
||||
vblank_frame: None,
|
||||
@@ -755,9 +813,16 @@ impl Tty {
|
||||
|
||||
niri.add_output(output.clone(), Some(refresh_interval(mode)));
|
||||
|
||||
self.enabled_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(output_name, output.clone());
|
||||
#[cfg(feature = "dbus")]
|
||||
niri.on_enabled_outputs_changed();
|
||||
|
||||
// Power on all monitors if necessary and queue a redraw on the new one.
|
||||
niri.event_loop.insert_idle(move |state| {
|
||||
state.niri.activate_monitors(&state.backend);
|
||||
state.niri.activate_monitors(&mut state.backend);
|
||||
state.niri.queue_redraw(output);
|
||||
});
|
||||
|
||||
@@ -794,6 +859,8 @@ impl Tty {
|
||||
};
|
||||
|
||||
self.enabled_outputs.lock().unwrap().remove(&surface.name);
|
||||
#[cfg(feature = "dbus")]
|
||||
niri.on_enabled_outputs_changed();
|
||||
}
|
||||
|
||||
fn on_vblank(
|
||||
@@ -1039,7 +1106,7 @@ impl Tty {
|
||||
|
||||
// Hand them over to the DRM.
|
||||
let drm_compositor = &mut surface.compositor;
|
||||
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
|
||||
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
|
||||
Ok(res) => {
|
||||
if self
|
||||
.config
|
||||
@@ -1058,7 +1125,7 @@ impl Tty {
|
||||
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
|
||||
}
|
||||
|
||||
if res.damage.is_some() {
|
||||
if !res.is_empty {
|
||||
let presentation_feedbacks =
|
||||
niri.take_presentation_feedbacks(output, &res.states);
|
||||
let data = (presentation_feedbacks, target_presentation_time);
|
||||
@@ -1126,20 +1193,20 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) {
|
||||
Ok(renderer) => renderer,
|
||||
Err(err) => {
|
||||
debug!("error creating renderer for primary GPU: {err:?}");
|
||||
return Err(());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match renderer.import_dmabuf(dmabuf, None) {
|
||||
Ok(_texture) => Ok(()),
|
||||
Ok(_texture) => true,
|
||||
Err(err) => {
|
||||
debug!("error importing dmabuf: {err:?}");
|
||||
Err(())
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,10 +1290,22 @@ impl Tty {
|
||||
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
|
||||
}
|
||||
|
||||
pub fn set_monitors_active(&self, active: bool) {
|
||||
for device in self.devices.values() {
|
||||
for crtc in device.surfaces.keys() {
|
||||
set_crtc_active(&device.drm, *crtc, active);
|
||||
pub fn set_monitors_active(&mut self, active: bool) {
|
||||
// We only disable the CRTC here, this will also reset the
|
||||
// surface state so that the next call to `render_frame` will
|
||||
// always produce a new frame and `queue_frame` will change
|
||||
// the CRTC to active. This makes sure we always enable a CRTC
|
||||
// within an atomic operation.
|
||||
if active {
|
||||
return;
|
||||
}
|
||||
|
||||
for device in self.devices.values_mut() {
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
set_crtc_active(&device.drm, *crtc, false);
|
||||
if let Err(err) = surface.compositor.reset_state() {
|
||||
warn!("error resetting surface state: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1234,6 +1313,13 @@ impl Tty {
|
||||
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::on_output_config_changed");
|
||||
|
||||
// If we're inactive, we can't do anything, so just set a flag for later.
|
||||
if !self.session.is_active() {
|
||||
self.update_output_config_on_resume = true;
|
||||
return;
|
||||
}
|
||||
self.update_output_config_on_resume = false;
|
||||
|
||||
let mut to_disconnect = vec![];
|
||||
let mut to_connect = vec![];
|
||||
|
||||
@@ -1255,12 +1341,11 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if we need to change the mode.
|
||||
let connector = surface
|
||||
.compositor
|
||||
.current_connectors()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
|
||||
else {
|
||||
error!("surface pending connectors is empty");
|
||||
continue;
|
||||
};
|
||||
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
|
||||
error!("missing enabled connector in drm_scanner");
|
||||
continue;
|
||||
@@ -1271,7 +1356,7 @@ impl Tty {
|
||||
continue;
|
||||
};
|
||||
|
||||
if surface.compositor.current_mode() == mode {
|
||||
if surface.compositor.pending_mode() == mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1365,6 +1450,10 @@ impl Tty {
|
||||
|
||||
self.refresh_ipc_outputs();
|
||||
}
|
||||
|
||||
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
||||
self.devices.get_mut(&node)
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
|
||||
@@ -1504,9 +1593,17 @@ fn refresh_interval(mode: DrmMode) -> Duration {
|
||||
#[cfg(feature = "dbus")]
|
||||
fn suspend() -> anyhow::Result<()> {
|
||||
let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?;
|
||||
let manager = logind_zbus::manager::ManagerProxyBlocking::new(&conn)
|
||||
.context("error creating login manager proxy")?;
|
||||
manager.suspend(true).context("error suspending")
|
||||
|
||||
conn.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Suspend",
|
||||
&(true),
|
||||
)
|
||||
.context("error suspending")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn queue_estimated_vblank_timer(
|
||||
|
||||
+13
-15
@@ -16,12 +16,10 @@ use smithay::reexports::calloop::LoopHandle;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::window::WindowBuilder;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use super::RenderResult;
|
||||
use crate::niri::{RedrawState, State};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::Niri;
|
||||
|
||||
pub struct Winit {
|
||||
config: Rc<RefCell<Config>>,
|
||||
@@ -33,12 +31,15 @@ pub struct Winit {
|
||||
}
|
||||
|
||||
impl Winit {
|
||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
|
||||
pub fn new(
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<State>,
|
||||
) -> Result<Self, winit::Error> {
|
||||
let builder = WindowBuilder::new()
|
||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||
// .with_resizable(false)
|
||||
.with_title("niri");
|
||||
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
||||
let (backend, winit) = winit::init_from_builder(builder)?;
|
||||
|
||||
let output = Output::new(
|
||||
"winit".to_string(),
|
||||
@@ -54,7 +55,7 @@ impl Winit {
|
||||
size: backend.window_size(),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
@@ -107,21 +108,18 @@ impl Winit {
|
||||
WinitEvent::Redraw => state
|
||||
.niri
|
||||
.queue_redraw(state.backend.winit().output.clone()),
|
||||
WinitEvent::CloseRequested => {
|
||||
state.niri.stop_signal.stop();
|
||||
state.niri.remove_output(&state.backend.winit().output);
|
||||
}
|
||||
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
config,
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
ipc_outputs,
|
||||
enabled_outputs,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
@@ -213,12 +211,12 @@ impl Winit {
|
||||
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
||||
Ok(_texture) => Ok(()),
|
||||
Ok(_texture) => true,
|
||||
Err(err) => {
|
||||
debug!("error importing dmabuf: {err:?}");
|
||||
Err(())
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use niri_ipc::Action;
|
||||
|
||||
use crate::utils::version;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version = version(), about, long_about = None)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||
#[command(subcommand_help_heading = "Subcommands")]
|
||||
pub struct Cli {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Command to run upon compositor startup.
|
||||
#[arg(last = true)]
|
||||
pub command: Vec<OsString>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Option<Sub>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
msg: Msg,
|
||||
/// Format output as JSON.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Msg {
|
||||
/// List connected outputs.
|
||||
Outputs,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
action: Action,
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::Config;
|
||||
use pangocairo::cairo::{self, ImageSurface};
|
||||
use pangocairo::pango::FontDescription;
|
||||
use smithay::backend::renderer::element::memory::{
|
||||
@@ -14,7 +17,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const TEXT: &str = "Failed to parse the config file. \
|
||||
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
||||
@@ -26,6 +29,12 @@ const BORDER: i32 = 4;
|
||||
pub struct ConfigErrorNotification {
|
||||
state: State,
|
||||
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
||||
|
||||
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
|
||||
// notification.
|
||||
created_path: Option<PathBuf>,
|
||||
|
||||
config: Rc<RefCell<Config>>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
@@ -39,16 +48,42 @@ pub type ConfigErrorNotificationRenderElement<R> =
|
||||
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||
|
||||
impl ConfigErrorNotification {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(config: Rc<RefCell<Config>>) -> Self {
|
||||
Self {
|
||||
state: State::Hidden,
|
||||
buffers: RefCell::new(HashMap::new()),
|
||||
created_path: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn animation(&self, from: f64, to: f64) -> Animation {
|
||||
let c = self.config.borrow();
|
||||
Animation::new(
|
||||
from,
|
||||
to,
|
||||
c.animations.config_notification_open_close,
|
||||
niri_config::Animation::default_config_notification_open_close(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
|
||||
if self.created_path != created_path {
|
||||
self.created_path = created_path;
|
||||
self.buffers.borrow_mut().clear();
|
||||
}
|
||||
|
||||
self.state = State::Showing(self.animation(0., 1.));
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
if self.created_path.is_some() {
|
||||
self.created_path = None;
|
||||
self.buffers.borrow_mut().clear();
|
||||
}
|
||||
|
||||
// Show from scratch even if already showing to bring attention.
|
||||
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
|
||||
self.state = State::Showing(self.animation(0., 1.));
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
@@ -56,7 +91,7 @@ impl ConfigErrorNotification {
|
||||
return;
|
||||
}
|
||||
|
||||
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
|
||||
self.state = State::Hiding(self.animation(1., 0.));
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
|
||||
@@ -65,7 +100,15 @@ impl ConfigErrorNotification {
|
||||
State::Showing(anim) => {
|
||||
anim.set_current_time(target_presentation_time);
|
||||
if anim.is_done() {
|
||||
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
|
||||
let duration = if self.created_path.is_some() {
|
||||
// Make this quite a bit longer because it comes with a monitor modeset
|
||||
// (can take a while) and an important hotkeys popup diverting the
|
||||
// attention.
|
||||
Duration::from_secs(8)
|
||||
} else {
|
||||
Duration::from_secs(4)
|
||||
};
|
||||
self.state = State::Shown(target_presentation_time + duration);
|
||||
}
|
||||
}
|
||||
State::Shown(deadline) => {
|
||||
@@ -96,11 +139,12 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let path = self.created_path.as_deref();
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let buffer = buffers
|
||||
.entry(scale)
|
||||
.or_insert_with_key(move |&scale| render(scale).ok());
|
||||
.or_insert_with_key(move |&scale| render(scale, path).ok());
|
||||
let buffer = buffer.as_ref()?;
|
||||
|
||||
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||
@@ -138,19 +182,30 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
let _span = tracy_client::span!("config_error_notification::render");
|
||||
|
||||
let padding = PADDING * scale;
|
||||
|
||||
let mut text = String::from(TEXT);
|
||||
let mut border_color = (1., 0.3, 0.3);
|
||||
if let Some(path) = created_path {
|
||||
text = format!(
|
||||
"Created a default config file at \
|
||||
<span face='monospace' bgcolor='#000000'>{:?}</span>",
|
||||
path
|
||||
);
|
||||
border_color = (0.5, 1., 0.5);
|
||||
};
|
||||
|
||||
let mut font = FontDescription::from_string(FONT);
|
||||
font.set_absolute_size((font.size() * scale).into());
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&text);
|
||||
|
||||
let (mut width, mut height) = layout.pixel_size();
|
||||
width += padding * 2;
|
||||
@@ -166,25 +221,25 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&text);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(0., 0.);
|
||||
cr.line_to(width.into(), 0.);
|
||||
cr.line_to(width.into(), height.into());
|
||||
cr.line_to(0., height.into());
|
||||
cr.line_to(0., 0.);
|
||||
cr.set_source_rgb(1., 0.3, 0.3);
|
||||
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
|
||||
cr.set_line_width((BORDER * scale).into());
|
||||
cr.stroke()?;
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
|
||||
+6
-20
@@ -8,8 +8,7 @@ use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::texture::TextureBuffer;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||
@@ -224,7 +223,7 @@ pub enum RenderCursor {
|
||||
},
|
||||
}
|
||||
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
|
||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CursorTextureCache {
|
||||
@@ -238,12 +237,11 @@ impl CursorTextureCache {
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
icon: CursorIcon,
|
||||
scale: i32,
|
||||
cursor: &XCursor,
|
||||
idx: usize,
|
||||
) -> Option<TextureBuffer<GlesTexture>> {
|
||||
) -> MemoryRenderBuffer {
|
||||
self.cache
|
||||
.borrow_mut()
|
||||
.entry((icon, scale))
|
||||
@@ -252,26 +250,14 @@ impl CursorTextureCache {
|
||||
.frames()
|
||||
.iter()
|
||||
.map(|frame| {
|
||||
let _span = tracy_client::span!("create TextureBuffer");
|
||||
|
||||
let buffer = TextureBuffer::from_memory(
|
||||
renderer,
|
||||
MemoryRenderBuffer::from_slice(
|
||||
&frame.pixels_rgba,
|
||||
Fourcc::Abgr8888,
|
||||
Fourcc::Argb8888,
|
||||
(frame.width as i32, frame.height as i32),
|
||||
false,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
|
||||
match buffer {
|
||||
Ok(x) => Some(x),
|
||||
Err(err) => {
|
||||
warn!("error creating a cursor texture: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})[idx]
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use anyhow::Context;
|
||||
use futures_util::StreamExt;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||
use zbus::zvariant::NoneValue;
|
||||
use zbus::{dbus_interface, MessageHeader, Task};
|
||||
|
||||
use super::Start;
|
||||
|
||||
pub struct ScreenSaver {
|
||||
is_inhibited: Arc<AtomicBool>,
|
||||
is_broken: Arc<AtomicBool>,
|
||||
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||
counter: u32,
|
||||
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
async fn inhibit(
|
||||
&mut self,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
application_name: &str,
|
||||
reason_for_inhibit: &str,
|
||||
) -> fdo::Result<u32> {
|
||||
trace!(
|
||||
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
|
||||
hdr.sender()
|
||||
);
|
||||
|
||||
let Ok(Some(name)) = hdr.sender() else {
|
||||
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||
};
|
||||
let name = OwnedUniqueName::from(name.to_owned());
|
||||
|
||||
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||
|
||||
let mut cookie = None;
|
||||
for _ in 0..3 {
|
||||
// Start from 1 because some clients don't like 0.
|
||||
self.counter = self.counter.wrapping_add(1);
|
||||
if self.counter == 0 {
|
||||
self.counter += 1;
|
||||
}
|
||||
|
||||
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
|
||||
entry.insert(name);
|
||||
self.is_inhibited.store(true, Ordering::SeqCst);
|
||||
cookie = Some(self.counter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
|
||||
}
|
||||
|
||||
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
|
||||
trace!("fdo uninhibit, cookie: {cookie}");
|
||||
|
||||
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||
|
||||
if inhibitors.remove(&cookie).is_some() {
|
||||
if inhibitors.is_empty() {
|
||||
self.is_inhibited.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(fdo::Error::Failed(String::from("invalid cookie")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenSaver {
|
||||
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
|
||||
Self {
|
||||
is_inhibited,
|
||||
is_broken: Arc::new(AtomicBool::new(false)),
|
||||
inhibitors: Arc::new(Mutex::new(HashMap::new())),
|
||||
counter: 0,
|
||||
monitor_task: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn monitor_disappeared_clients(
|
||||
conn: &zbus::Connection,
|
||||
is_inhibited: Arc<AtomicBool>,
|
||||
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let proxy = fdo::DBusProxy::new(conn)
|
||||
.await
|
||||
.context("error creating a DBusProxy")?;
|
||||
|
||||
let mut stream = proxy
|
||||
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
|
||||
.await
|
||||
.context("error creating a NameOwnerChanged stream")?;
|
||||
|
||||
while let Some(signal) = stream.next().await {
|
||||
let args = signal
|
||||
.args()
|
||||
.context("error retrieving NameOwnerChanged args")?;
|
||||
|
||||
let Some(name) = &**args.old_owner() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if args.new_owner().is_none() {
|
||||
trace!("fdo ScreenSaver client disappeared: {name}");
|
||||
|
||||
let mut inhibitors = inhibitors.lock().unwrap();
|
||||
inhibitors.retain(|_, owner| owner != name);
|
||||
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
|
||||
} else {
|
||||
error!("non-null new_owner should've been filtered out");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Start for ScreenSaver {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let is_inhibited = self.is_inhibited.clone();
|
||||
let is_broken = self.is_broken.clone();
|
||||
let inhibitors = self.inhibitors.clone();
|
||||
let monitor_task = self.monitor_task.clone();
|
||||
|
||||
let conn = zbus::blocking::Connection::session()?;
|
||||
let flags = RequestNameFlags::AllowReplacement
|
||||
| RequestNameFlags::ReplaceExisting
|
||||
| RequestNameFlags::DoNotQueue;
|
||||
|
||||
conn.object_server()
|
||||
.at("/org/freedesktop/ScreenSaver", self)?;
|
||||
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
|
||||
|
||||
let async_conn = conn.inner();
|
||||
let future = {
|
||||
let conn = async_conn.clone();
|
||||
async move {
|
||||
if let Err(err) =
|
||||
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
|
||||
.await
|
||||
{
|
||||
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
|
||||
is_broken.store(true, Ordering::SeqCst);
|
||||
is_inhibited.store(false, Ordering::SeqCst);
|
||||
inhibitors.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
let task = async_conn
|
||||
.executor()
|
||||
.spawn(future, "monitor disappearing clients");
|
||||
monitor_task.set(task).unwrap();
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use zbus::Interface;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub mod freedesktop_screensaver;
|
||||
pub mod gnome_shell_screenshot;
|
||||
pub mod mutter_display_config;
|
||||
pub mod mutter_service_channel;
|
||||
@@ -13,6 +14,7 @@ pub mod mutter_screen_cast;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
use mutter_screen_cast::ScreenCast;
|
||||
|
||||
use self::freedesktop_screensaver::ScreenSaver;
|
||||
use self::mutter_display_config::DisplayConfig;
|
||||
use self::mutter_service_channel::ServiceChannel;
|
||||
|
||||
@@ -24,6 +26,7 @@ trait Start: Interface {
|
||||
pub struct DBusServers {
|
||||
pub conn_service_channel: Option<Connection>,
|
||||
pub conn_display_config: Option<Connection>,
|
||||
pub conn_screen_saver: Option<Connection>,
|
||||
pub conn_screen_shot: Option<Connection>,
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub conn_screen_cast: Option<Connection>,
|
||||
@@ -48,6 +51,9 @@ impl DBusServers {
|
||||
let display_config = DisplayConfig::new(backend.enabled_outputs());
|
||||
dbus.conn_display_config = try_start(display_config);
|
||||
|
||||
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||
dbus.conn_screen_saver = try_start(screen_saver);
|
||||
|
||||
let (to_niri, from_screenshot) = calloop::channel::channel();
|
||||
let (to_screenshot, from_niri) = async_channel::unbounded();
|
||||
niri.event_loop
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::Serialize;
|
||||
use smithay::output::Output;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
use zbus::{dbus_interface, fdo};
|
||||
use zbus::{dbus_interface, fdo, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -112,7 +112,8 @@ impl DisplayConfig {
|
||||
Ok((0, monitors, logical_monitors, HashMap::new()))
|
||||
}
|
||||
|
||||
// FIXME: monitors-changed signal.
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
|
||||
@@ -7,10 +7,11 @@ use serde::Deserialize;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
use crate::utils::output_size;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenCast {
|
||||
@@ -54,6 +55,13 @@ pub struct Stream {
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct StreamParameters {
|
||||
/// Size of the stream in logical coordinates.
|
||||
size: (i32, i32),
|
||||
}
|
||||
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
@@ -195,6 +203,12 @@ impl Stream {
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(property)]
|
||||
async fn parameters(&self) -> StreamParameters {
|
||||
let size = output_size(&self.output).into();
|
||||
StreamParameters { size }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCast {
|
||||
|
||||
@@ -12,7 +12,7 @@ use smithay::output::Output;
|
||||
use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
||||
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
||||
@@ -111,7 +111,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
@@ -130,13 +130,13 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(0., 0.);
|
||||
cr.line_to(width.into(), 0.);
|
||||
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
|
||||
@@ -97,15 +97,34 @@ impl CompositorHandler for State {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if is_mapped {
|
||||
// The toplevel got mapped.
|
||||
let window = entry.remove();
|
||||
window.on_commit();
|
||||
|
||||
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
|
||||
{
|
||||
let parent = window
|
||||
.toplevel()
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(win, _)| win.clone());
|
||||
|
||||
let win = window.clone();
|
||||
|
||||
// Open dialogs immediately to the right of their parent window.
|
||||
let output = if let Some(p) = parent {
|
||||
self.niri.layout.add_window_right_of(&p, win, None, false)
|
||||
} else {
|
||||
self.niri.layout.add_window(win, None, false)
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
self.niri.layout.start_open_animation_for_window(&window);
|
||||
self.niri.queue_redraw(output);
|
||||
}
|
||||
return;
|
||||
@@ -125,7 +144,11 @@ impl CompositorHandler for State {
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if !is_mapped {
|
||||
// The toplevel got unmapped.
|
||||
|
||||
+159
-12
@@ -9,10 +9,13 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::input;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -20,7 +23,13 @@ use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
@@ -39,13 +48,18 @@ use smithay::wayland::session_lock::{
|
||||
};
|
||||
use smithay::{
|
||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
||||
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
|
||||
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
|
||||
delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
||||
};
|
||||
|
||||
use crate::delegate_foreign_toplevel;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
use crate::utils::output_size;
|
||||
|
||||
impl SeatHandler for State {
|
||||
@@ -73,6 +87,19 @@ impl SeatHandler for State {
|
||||
set_data_device_focus(dh, seat, client.clone());
|
||||
set_primary_focus(dh, seat, client);
|
||||
}
|
||||
|
||||
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
|
||||
let keyboards = self
|
||||
.niri
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
|
||||
.cloned();
|
||||
|
||||
for mut keyboard in keyboards {
|
||||
keyboard.led_update(led_state.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_seat!(State);
|
||||
delegate_cursor_shape!(State);
|
||||
@@ -189,6 +216,11 @@ impl DataControlHandler for State {
|
||||
|
||||
delegate_data_control!(State);
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
||||
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
||||
}
|
||||
}
|
||||
delegate_output!(State);
|
||||
|
||||
delegate_presentation!(State);
|
||||
@@ -204,13 +236,10 @@ impl DmabufHandler for State {
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
match self.backend.import_dmabuf(&dmabuf) {
|
||||
Ok(_) => {
|
||||
let _ = notifier.successful::<State>();
|
||||
}
|
||||
Err(_) => {
|
||||
notifier.failed();
|
||||
}
|
||||
if self.backend.import_dmabuf(&dmabuf) {
|
||||
let _ = notifier.successful::<State>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,3 +306,121 @@ impl SecurityContextHandler for State {
|
||||
}
|
||||
}
|
||||
delegate_security_context!(State);
|
||||
|
||||
impl IdleNotifierHandler for State {
|
||||
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
||||
&mut self.niri.idle_notifier_state
|
||||
}
|
||||
}
|
||||
delegate_idle_notify!(State);
|
||||
|
||||
impl IdleInhibitHandler for State {
|
||||
fn inhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.insert(surface);
|
||||
}
|
||||
|
||||
fn uninhibit(&mut self, surface: WlSurface) {
|
||||
self.niri.idle_inhibiting_surfaces.remove(&surface);
|
||||
}
|
||||
}
|
||||
delegate_idle_inhibit!(State);
|
||||
|
||||
impl ForeignToplevelHandler for State {
|
||||
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
|
||||
&mut self.niri.foreign_toplevel_state
|
||||
}
|
||||
|
||||
fn activate(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
window.toplevel().send_close();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||
{
|
||||
if !window
|
||||
.toplevel()
|
||||
.current_state()
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let window = window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(window.clone(), &requested_output);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
|
||||
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_foreign_toplevel!(State);
|
||||
|
||||
impl DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
&mut self
|
||||
.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.drm_lease_state
|
||||
}
|
||||
|
||||
fn lease_request(
|
||||
&mut self,
|
||||
node: DrmNode,
|
||||
request: DrmLeaseRequest,
|
||||
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||
debug!(
|
||||
"Received lease request for {} connectors",
|
||||
request.connectors.len()
|
||||
);
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.lease_request(request)
|
||||
}
|
||||
|
||||
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
|
||||
debug!("Lease success");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.new_lease(lease);
|
||||
}
|
||||
|
||||
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
|
||||
debug!("Destroyed lease");
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.remove_lease(lease_id);
|
||||
}
|
||||
}
|
||||
delegate_drm_lease!(State);
|
||||
|
||||
@@ -13,6 +13,7 @@ use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
@@ -94,6 +95,15 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||
// replace each other.
|
||||
// FIXME: do this properly.
|
||||
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||
return;
|
||||
}
|
||||
|
||||
let popup = PopupKind::Xdg(surface);
|
||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||
return;
|
||||
@@ -220,6 +230,13 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
|
||||
if let Some(ws) = self.niri.layout.active_workspace() {
|
||||
window.toplevel().with_pending_state(|state| {
|
||||
state.size = Some(ws.view_size());
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +253,13 @@ impl XdgShellHandler for State {
|
||||
{
|
||||
let window = window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
|
||||
if let Some(ws) = self.niri.layout.active_workspace() {
|
||||
window.toplevel().with_pending_state(|state| {
|
||||
state.size = Some(ws.new_window_size());
|
||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-18
@@ -18,7 +18,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::{Physical, Size, Transform};
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::render_helpers::NiriRenderer;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
const PADDING: i32 = 8;
|
||||
const MARGIN: i32 = PADDING * 2;
|
||||
@@ -155,13 +155,26 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
let binds = &config.binds.0;
|
||||
|
||||
// Collect actions that we want to show.
|
||||
let mut actions = vec![
|
||||
&Action::ShowHotkeyOverlay,
|
||||
&Action::Quit,
|
||||
&Action::CloseWindow,
|
||||
];
|
||||
let mut actions = vec![&Action::ShowHotkeyOverlay];
|
||||
|
||||
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
|
||||
// Quit(false).
|
||||
if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::Quit(false)))
|
||||
{
|
||||
actions.push(&Action::Quit(false));
|
||||
} else if binds
|
||||
.iter()
|
||||
.any(|bind| bind.actions.first() == Some(&Action::Quit(true)))
|
||||
{
|
||||
actions.push(&Action::Quit(true));
|
||||
} else {
|
||||
actions.push(&Action::Quit(false));
|
||||
}
|
||||
|
||||
actions.extend(&[
|
||||
&Action::CloseWindow,
|
||||
&Action::FocusColumnLeft,
|
||||
&Action::FocusColumnRight,
|
||||
&Action::MoveColumnLeft,
|
||||
@@ -216,12 +229,21 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
}
|
||||
|
||||
// Add the spawn actions.
|
||||
for bind in binds
|
||||
.iter()
|
||||
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
|
||||
{
|
||||
actions.push(bind.actions.first().unwrap());
|
||||
let mut spawn_actions = Vec::new();
|
||||
for bind in binds.iter().filter(|bind| {
|
||||
matches!(bind.actions.first(), Some(Action::Spawn(_)))
|
||||
// Only show binds with Mod or Super to filter out stuff like volume up/down.
|
||||
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|
||||
|| bind.key.modifiers.contains(Modifiers::SUPER))
|
||||
}) {
|
||||
let action = bind.actions.first().unwrap();
|
||||
|
||||
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
|
||||
if !spawn_actions.contains(&action) {
|
||||
spawn_actions.push(action);
|
||||
}
|
||||
}
|
||||
actions.extend(spawn_actions);
|
||||
|
||||
let strings = actions
|
||||
.into_iter()
|
||||
@@ -243,7 +265,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
let bold = AttrList::new();
|
||||
@@ -298,7 +320,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::create_layout(&cr);
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
@@ -306,20 +328,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
|
||||
layout.set_attributes(Some(&bold));
|
||||
layout.set_text(TITLE);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
|
||||
|
||||
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
|
||||
layout.set_attributes(Some(&attrs));
|
||||
layout.set_text(key);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.rel_move_to((key_width + padding).into(), 0.);
|
||||
|
||||
layout.set_attributes(None);
|
||||
layout.set_markup(action);
|
||||
pangocairo::show_layout(&cr, &layout);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.rel_move_to(
|
||||
(-(key_width + padding)).into(),
|
||||
@@ -338,7 +360,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = MemoryRenderBuffer::from_memory(
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
@@ -356,7 +378,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
||||
|
||||
fn action_name(action: &Action) -> String {
|
||||
match action {
|
||||
Action::Quit => String::from("Exit niri"),
|
||||
Action::Quit(_) => String::from("Exit niri"),
|
||||
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
|
||||
Action::CloseWindow => String::from("Close Focused Window"),
|
||||
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
|
||||
|
||||
+282
-26
@@ -1,7 +1,8 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use niri_config::{Action, Binds, LayoutAction, Modifiers};
|
||||
use niri_config::{Action, Binds, Modifiers};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
|
||||
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
|
||||
@@ -49,9 +50,24 @@ impl State {
|
||||
// here.
|
||||
self.niri.layout.advance_animations(get_monotonic_time());
|
||||
|
||||
// Power on monitors if they were off.
|
||||
if should_activate_monitors(&event) {
|
||||
self.niri.activate_monitors(&self.backend);
|
||||
if self.niri.monitors_active {
|
||||
// Notify the idle-notifier of activity.
|
||||
if should_notify_activity(&event) {
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
} else {
|
||||
// Power on monitors if they were off.
|
||||
if should_activate_monitors(&event) {
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
|
||||
// Notify the idle-notifier of activity only if we're also powering on the
|
||||
// monitors.
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
}
|
||||
|
||||
let hide_hotkey_overlay =
|
||||
@@ -90,6 +106,7 @@ impl State {
|
||||
TouchUp { .. } => (),
|
||||
TouchCancel { .. } => (),
|
||||
TouchFrame { .. } => (),
|
||||
SwitchToggle { .. } => (),
|
||||
Special(_) => (),
|
||||
}
|
||||
|
||||
@@ -126,6 +143,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if device.has_capability(input::DeviceCapability::Keyboard) {
|
||||
if let Some(led_state) = self
|
||||
.niri
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.map(|keyboard| keyboard.led_state())
|
||||
{
|
||||
device.led_update(led_state.into());
|
||||
}
|
||||
}
|
||||
|
||||
apply_libinput_settings(&self.niri.config.borrow().input, device);
|
||||
}
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
@@ -220,6 +248,7 @@ impl State {
|
||||
|
||||
if let Some(dialog) = &this.niri.exit_confirm_dialog {
|
||||
if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
|
||||
info!("quitting after confirming exit dialog");
|
||||
this.niri.stop_signal.stop();
|
||||
}
|
||||
}
|
||||
@@ -246,24 +275,31 @@ impl State {
|
||||
return;
|
||||
}
|
||||
|
||||
self.do_action(action);
|
||||
}
|
||||
|
||||
pub fn do_action(&mut self, action: Action) {
|
||||
if self.niri.is_locked() && !allowed_when_locked(&action) {
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
Action::Quit => {
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if dialog.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
Action::Quit(skip_confirmation) => {
|
||||
if !skip_confirmation {
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if dialog.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
info!("quitting because quit bind was pressed");
|
||||
self.niri.stop_signal.stop()
|
||||
}
|
||||
|
||||
info!("quitting as requested");
|
||||
self.niri.stop_signal.stop()
|
||||
}
|
||||
Action::ChangeVt(vt) => {
|
||||
self.backend.change_vt(vt);
|
||||
// Changing `VT` may not deliver the key releases, so clear the state.
|
||||
// Changing VT may not deliver the key releases, so clear the state.
|
||||
self.niri.suppressed_keys.clear();
|
||||
}
|
||||
Action::Suspend => {
|
||||
@@ -272,7 +308,7 @@ impl State {
|
||||
self.niri.suppressed_keys.clear();
|
||||
}
|
||||
Action::PowerOffMonitors => {
|
||||
self.niri.deactivate_monitors(&self.backend);
|
||||
self.niri.deactivate_monitors(&mut self.backend);
|
||||
}
|
||||
Action::ToggleDebugTint => {
|
||||
self.backend.toggle_debug_tint();
|
||||
@@ -350,8 +386,8 @@ impl State {
|
||||
self.niri.seat.get_keyboard().unwrap().with_xkb_state(
|
||||
self,
|
||||
|mut state| match action {
|
||||
LayoutAction::Next => state.cycle_next_layout(),
|
||||
LayoutAction::Prev => state.cycle_prev_layout(),
|
||||
LayoutSwitchTarget::Next => state.cycle_next_layout(),
|
||||
LayoutSwitchTarget::Prev => state.cycle_prev_layout(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -395,6 +431,16 @@ impl State {
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::ConsumeOrExpelWindowLeft => {
|
||||
self.niri.layout.consume_or_expel_window_left();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::ConsumeOrExpelWindowRight => {
|
||||
self.niri.layout.consume_or_expel_window_right();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::FocusColumnLeft => {
|
||||
self.niri.layout.focus_left();
|
||||
// FIXME: granular
|
||||
@@ -597,6 +643,30 @@ impl State {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorLeft => {
|
||||
if let Some(output) = self.niri.output_left() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorRight => {
|
||||
if let Some(output) = self.niri.output_right() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorDown => {
|
||||
if let Some(output) = self.niri.output_down() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorUp => {
|
||||
if let Some(output) = self.niri.output_up() {
|
||||
self.niri.layout.move_workspace_to_output(&output);
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,11 +1190,20 @@ impl State {
|
||||
);
|
||||
}
|
||||
|
||||
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
|
||||
let res = self
|
||||
.niri
|
||||
.layout
|
||||
.workspace_switch_gesture_update(event.delta_y());
|
||||
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent)
|
||||
where
|
||||
I::Device: 'static,
|
||||
{
|
||||
let mut delta_y = event.delta_y();
|
||||
|
||||
let device = event.device();
|
||||
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
|
||||
if device.config_scroll_natural_scroll_enabled() {
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
}
|
||||
|
||||
let res = self.niri.layout.workspace_switch_gesture_update(delta_y);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(output);
|
||||
@@ -1351,6 +1430,15 @@ fn action(
|
||||
_ => (),
|
||||
}
|
||||
|
||||
bound_action(bindings, comp_mod, raw, mods)
|
||||
}
|
||||
|
||||
fn bound_action(
|
||||
bindings: &Binds,
|
||||
comp_mod: CompositorMod,
|
||||
raw: Option<Keysym>,
|
||||
mods: ModifiersState,
|
||||
) -> Option<Action> {
|
||||
// Handle configured binds.
|
||||
let mut modifiers = Modifiers::empty();
|
||||
if mods.ctrl {
|
||||
@@ -1366,14 +1454,12 @@ fn action(
|
||||
modifiers |= Modifiers::SUPER;
|
||||
}
|
||||
|
||||
let (mod_down, mut comp_mod) = match comp_mod {
|
||||
let (mod_down, comp_mod) = match comp_mod {
|
||||
CompositorMod::Super => (mods.logo, Modifiers::SUPER),
|
||||
CompositorMod::Alt => (mods.alt, Modifiers::ALT),
|
||||
};
|
||||
if mod_down {
|
||||
modifiers |= Modifiers::COMPOSITOR;
|
||||
} else {
|
||||
comp_mod = Modifiers::empty();
|
||||
}
|
||||
|
||||
let raw = raw?;
|
||||
@@ -1383,7 +1469,14 @@ fn action(
|
||||
continue;
|
||||
}
|
||||
|
||||
if bind.key.modifiers | comp_mod == modifiers {
|
||||
let mut bind_modifiers = bind.key.modifiers;
|
||||
if bind_modifiers.contains(Modifiers::COMPOSITOR) {
|
||||
bind_modifiers |= comp_mod;
|
||||
} else if bind_modifiers.contains(comp_mod) {
|
||||
bind_modifiers |= Modifiers::COMPOSITOR;
|
||||
}
|
||||
|
||||
if bind_modifiers == modifiers {
|
||||
return bind.actions.first().cloned();
|
||||
}
|
||||
}
|
||||
@@ -1442,10 +1535,17 @@ fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bo
|
||||
}
|
||||
}
|
||||
|
||||
fn should_notify_activity<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
!matches!(
|
||||
event,
|
||||
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn allowed_when_locked(action: &Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
Action::Quit
|
||||
Action::Quit(_)
|
||||
| Action::ChangeVt(_)
|
||||
| Action::Suspend
|
||||
| Action::PowerOffMonitors
|
||||
@@ -1456,7 +1556,7 @@ fn allowed_when_locked(action: &Action) -> bool {
|
||||
fn allowed_during_screenshot(action: &Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
|
||||
Action::Quit(_) | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1467,6 +1567,7 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let c = &config.touchpad;
|
||||
let _ = device.config_tap_set_enabled(c.tap);
|
||||
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||
let _ = device.config_dwtp_set_enabled(c.dwtp);
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed);
|
||||
|
||||
@@ -1642,4 +1743,159 @@ mod tests {
|
||||
// Ensure that no keys are being suppressed.
|
||||
assert!(suppressed_keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comp_mod_handling() {
|
||||
let bindings = Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::q,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
actions: vec![Action::CloseWindow],
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::h,
|
||||
modifiers: Modifiers::SUPER,
|
||||
},
|
||||
actions: vec![Action::FocusColumnLeft],
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::j,
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
actions: vec![Action::FocusWindowDown],
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::k,
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
|
||||
},
|
||||
actions: vec![Action::FocusWindowUp],
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::l,
|
||||
modifiers: Modifiers::SUPER | Modifiers::ALT,
|
||||
},
|
||||
actions: vec![Action::FocusColumnRight],
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::CloseWindow)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnLeft)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
Some(Action::FocusWindowDown)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusWindowUp)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnRight)
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-8
@@ -3,10 +3,10 @@ use std::io::{Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use niri_ipc::{Mode, Output, Request, Response};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_ipc::{Mode, Output, Reply, Request, Response};
|
||||
|
||||
use crate::Msg;
|
||||
use crate::cli::Msg;
|
||||
|
||||
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
||||
@@ -19,8 +19,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let mut stream =
|
||||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
||||
|
||||
let request = match msg {
|
||||
let request = match &msg {
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::Action { action } => Request::Action(action.clone()),
|
||||
};
|
||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||
stream
|
||||
@@ -35,12 +36,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
.read_to_end(&mut buf)
|
||||
.context("error reading IPC response")?;
|
||||
|
||||
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
|
||||
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
|
||||
|
||||
let response = reply
|
||||
.map_err(|msg| anyhow!(msg))
|
||||
.context("niri could not handle the request")?;
|
||||
|
||||
match msg {
|
||||
Msg::Outputs => {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let Response::Outputs(outputs) = response
|
||||
else {
|
||||
let Response::Outputs(outputs) = response else {
|
||||
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||
};
|
||||
|
||||
@@ -100,6 +104,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+23
-8
@@ -22,6 +22,7 @@ pub struct IpcServer {
|
||||
}
|
||||
|
||||
struct ClientCtx {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
};
|
||||
|
||||
let ctx = ClientCtx {
|
||||
event_loop: state.niri.event_loop.clone(),
|
||||
ipc_outputs: state.backend.ipc_outputs(),
|
||||
};
|
||||
|
||||
@@ -108,20 +110,33 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
|
||||
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
|
||||
let reply = process(&ctx, &buf).map_err(|err| {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
err.to_string()
|
||||
});
|
||||
|
||||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
|
||||
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
|
||||
|
||||
let response = match request {
|
||||
Request::Outputs => {
|
||||
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
|
||||
Response::Outputs(ipc_outputs)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.do_action(action);
|
||||
});
|
||||
Response::Handled
|
||||
}
|
||||
};
|
||||
|
||||
let buf = serde_json::to_vec(&response).context("error formatting response")?;
|
||||
write
|
||||
.write_all(&buf)
|
||||
.await
|
||||
.context("error writing response")?;
|
||||
|
||||
Ok(())
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
+449
-45
@@ -29,13 +29,16 @@
|
||||
//! compromise we only keep the first workspace there, and move the rest to the primary output,
|
||||
//! making the primary output their original output.
|
||||
|
||||
use std::cmp::min;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{self, CenterFocusedColumn, Config, SizeChange, Struts};
|
||||
use smithay::backend::renderer::element::AsRenderElements;
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use niri_config::{self, CenterFocusedColumn, Config, Struts};
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::{AsRenderElements, Id};
|
||||
use smithay::desktop::space::SpaceElement;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::output::Output;
|
||||
@@ -48,16 +51,25 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
pub use self::monitor::MonitorRenderElement;
|
||||
use self::monitor::{Monitor, WorkspaceSwitch, WorkspaceSwitchGesture};
|
||||
use self::workspace::{
|
||||
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
||||
};
|
||||
use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace};
|
||||
use crate::animation::Animation;
|
||||
use crate::niri::WindowOffscreenId;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::nearest_integer_scale::NearestIntegerScale;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::output_size;
|
||||
|
||||
mod focus_ring;
|
||||
mod monitor;
|
||||
mod tile;
|
||||
mod workspace;
|
||||
pub mod focus_ring;
|
||||
pub mod monitor;
|
||||
pub mod tile;
|
||||
pub mod workspace;
|
||||
|
||||
niri_render_elements! {
|
||||
LayoutElementRenderElement => {
|
||||
Wayland = NearestIntegerScale<WaylandSurfaceRenderElement<R>>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LayoutElement: PartialEq {
|
||||
/// Visual size of the element.
|
||||
@@ -80,14 +92,12 @@ pub trait LayoutElement: PartialEq {
|
||||
///
|
||||
/// The element should be rendered in such a way that its visual geometry ends up at the given
|
||||
/// location.
|
||||
fn render<R: Renderer + ImportAll>(
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<WorkspaceRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static;
|
||||
) -> Vec<LayoutElementRenderElement<R>>;
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>);
|
||||
fn request_fullscreen(&self, size: Size<i32, Logical>);
|
||||
@@ -98,11 +108,17 @@ pub trait LayoutElement: PartialEq {
|
||||
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform);
|
||||
fn output_enter(&self, output: &Output);
|
||||
fn output_leave(&self, output: &Output);
|
||||
fn set_offscreen_element_id(&self, id: Option<Id>);
|
||||
|
||||
/// Whether the element is currently fullscreen.
|
||||
///
|
||||
/// This will *not* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
|
||||
fn is_fullscreen(&self) -> bool;
|
||||
|
||||
/// Whether we're requesting the element to be fullscreen.
|
||||
///
|
||||
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
|
||||
fn is_pending_fullscreen(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -134,16 +150,17 @@ enum MonitorSet<W: LayoutElement> {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Options {
|
||||
/// Padding around windows in logical pixels.
|
||||
gaps: i32,
|
||||
pub gaps: i32,
|
||||
/// Extra padding around the working area in logical pixels.
|
||||
struts: Struts,
|
||||
focus_ring: niri_config::FocusRing,
|
||||
border: niri_config::FocusRing,
|
||||
center_focused_column: CenterFocusedColumn,
|
||||
pub struts: Struts,
|
||||
pub focus_ring: niri_config::FocusRing,
|
||||
pub border: niri_config::Border,
|
||||
pub center_focused_column: CenterFocusedColumn,
|
||||
/// Column widths that `toggle_width()` switches between.
|
||||
preset_widths: Vec<ColumnWidth>,
|
||||
pub preset_widths: Vec<ColumnWidth>,
|
||||
/// Initial width for new columns.
|
||||
default_width: Option<ColumnWidth>,
|
||||
pub default_width: Option<ColumnWidth>,
|
||||
pub animations: niri_config::Animations,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
@@ -152,7 +169,7 @@ impl Default for Options {
|
||||
gaps: 16,
|
||||
struts: Default::default(),
|
||||
focus_ring: Default::default(),
|
||||
border: niri_config::default_border(),
|
||||
border: Default::default(),
|
||||
center_focused_column: Default::default(),
|
||||
preset_widths: vec![
|
||||
ColumnWidth::Proportion(1. / 3.),
|
||||
@@ -160,6 +177,7 @@ impl Default for Options {
|
||||
ColumnWidth::Proportion(2. / 3.),
|
||||
],
|
||||
default_width: None,
|
||||
animations: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +213,7 @@ impl Options {
|
||||
center_focused_column: layout.center_focused_column,
|
||||
preset_widths,
|
||||
default_width,
|
||||
animations: config.animations,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,22 +232,24 @@ impl LayoutElement for Window {
|
||||
SpaceElement::is_in_input_region(self, &surace_local)
|
||||
}
|
||||
|
||||
fn render<R: Renderer + ImportAll>(
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<WorkspaceRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
let buf_pos = location - self.geometry().loc;
|
||||
self.render_elements(
|
||||
let elements: Vec<WaylandSurfaceRenderElement<R>> = self.render_elements(
|
||||
renderer,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
)
|
||||
);
|
||||
elements
|
||||
.into_iter()
|
||||
.map(NearestIntegerScale::from)
|
||||
.map(LayoutElementRenderElement::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>) {
|
||||
@@ -283,19 +304,33 @@ impl LayoutElement for Window {
|
||||
SpaceElement::output_leave(self, output)
|
||||
}
|
||||
|
||||
fn set_offscreen_element_id(&self, id: Option<Id>) {
|
||||
let data = self.user_data().get_or_insert(WindowOffscreenId::default);
|
||||
data.0.replace(id);
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
self.toplevel()
|
||||
.current_state()
|
||||
.states
|
||||
.contains(xdg_toplevel::State::Fullscreen)
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
self.toplevel()
|
||||
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Layout<W> {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
Self::with_options(Options::from_config(config))
|
||||
}
|
||||
|
||||
pub fn with_options(options: Options) -> Self {
|
||||
Self {
|
||||
monitor_set: MonitorSet::NoOutputs { workspaces: vec![] },
|
||||
options: Rc::new(Options::from_config(config)),
|
||||
options: Rc::new(options),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +581,43 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new window to the layout immediately to the right of another window.
|
||||
///
|
||||
/// If that another window was active, activates the new window.
|
||||
///
|
||||
/// Returns an output that the window was added to, if there were any outputs.
|
||||
pub fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &W,
|
||||
window: W,
|
||||
width: Option<ColumnWidth>,
|
||||
is_full_width: bool,
|
||||
) -> Option<&Output> {
|
||||
let width = width
|
||||
.or(self.options.default_width)
|
||||
.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
|
||||
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
let mon = monitors
|
||||
.iter_mut()
|
||||
.find(|mon| mon.workspaces.iter().any(|ws| ws.has_window(right_of)))
|
||||
.unwrap();
|
||||
|
||||
mon.add_window_right_of(right_of, window, width, is_full_width);
|
||||
Some(&mon.output)
|
||||
}
|
||||
MonitorSet::NoOutputs { workspaces } => {
|
||||
let ws = workspaces
|
||||
.iter_mut()
|
||||
.find(|ws| ws.has_window(right_of))
|
||||
.unwrap();
|
||||
ws.add_window_right_of(right_of, window, width, is_full_width);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window: &W) {
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
@@ -776,6 +848,27 @@ impl<W: LayoutElement> Layout<W> {
|
||||
mon.workspaces.iter().flat_map(|ws| ws.windows())
|
||||
}
|
||||
|
||||
pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>)) {
|
||||
match &self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
for mon in monitors {
|
||||
for ws in &mon.workspaces {
|
||||
for win in ws.windows() {
|
||||
f(win, Some(&mon.output));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MonitorSet::NoOutputs { workspaces } => {
|
||||
for ws in workspaces {
|
||||
for win in ws.windows() {
|
||||
f(win, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
|
||||
let MonitorSet::Normal {
|
||||
monitors,
|
||||
@@ -863,6 +956,20 @@ impl<W: LayoutElement> Layout<W> {
|
||||
monitor.move_up_or_to_workspace_up();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_left(&mut self) {
|
||||
let Some(monitor) = self.active_monitor() else {
|
||||
return;
|
||||
};
|
||||
monitor.consume_or_expel_window_left();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_right(&mut self) {
|
||||
let Some(monitor) = self.active_monitor() else {
|
||||
return;
|
||||
};
|
||||
monitor.consume_or_expel_window_right();
|
||||
}
|
||||
|
||||
pub fn focus_left(&mut self) {
|
||||
let Some(monitor) = self.active_monitor() else {
|
||||
return;
|
||||
@@ -1314,6 +1421,47 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_workspace_to_output(&mut self, output: &Output) {
|
||||
let MonitorSet::Normal {
|
||||
monitors,
|
||||
active_monitor_idx,
|
||||
..
|
||||
} = &mut self.monitor_set
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current = &mut monitors[*active_monitor_idx];
|
||||
if current.active_workspace_idx == current.workspaces.len() - 1 {
|
||||
// Insert a new empty workspace.
|
||||
let ws = Workspace::new(current.output.clone(), current.options.clone());
|
||||
current.workspaces.push(ws);
|
||||
}
|
||||
let mut ws = current.workspaces.remove(current.active_workspace_idx);
|
||||
current.active_workspace_idx = current.active_workspace_idx.saturating_sub(1);
|
||||
current.workspace_switch = None;
|
||||
current.clean_up_workspaces();
|
||||
|
||||
ws.set_output(Some(output.clone()));
|
||||
ws.original_output = OutputId::new(output);
|
||||
|
||||
let target_idx = monitors
|
||||
.iter()
|
||||
.position(|mon| &mon.output == output)
|
||||
.unwrap();
|
||||
let target = &mut monitors[target_idx];
|
||||
|
||||
// Insert the workspace after the currently active one. Unless the currently active one is
|
||||
// the last empty workspace, then insert before.
|
||||
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
|
||||
target.workspaces.insert(target_ws_idx, ws);
|
||||
target.active_workspace_idx = target_ws_idx;
|
||||
target.workspace_switch = None;
|
||||
target.clean_up_workspaces();
|
||||
|
||||
*active_monitor_idx = target_idx;
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
@@ -1399,7 +1547,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
for monitor in monitors {
|
||||
if let Some(WorkspaceSwitch::Gesture(gesture)) = &mut monitor.workspace_switch {
|
||||
// Normalize like GNOME Shell's workspace switching.
|
||||
let delta_y = -delta_y / 400.;
|
||||
let delta_y = delta_y / 400.;
|
||||
|
||||
let min = gesture.center_idx.saturating_sub(1) as f64;
|
||||
let max = (gesture.center_idx + 1).min(monitor.workspaces.len() - 1) as f64;
|
||||
@@ -1439,7 +1587,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
monitor.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||
current_idx,
|
||||
idx as f64,
|
||||
Duration::from_millis(250),
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
|
||||
return Some(monitor.output.clone());
|
||||
@@ -1462,6 +1611,39 @@ impl<W: LayoutElement> Layout<W> {
|
||||
};
|
||||
monitor.move_workspace_up();
|
||||
}
|
||||
|
||||
pub fn start_open_animation_for_window(&mut self, window: &W) {
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
for mon in monitors {
|
||||
for ws in &mut mon.workspaces {
|
||||
for col in &mut ws.columns {
|
||||
for tile in &mut col.tiles {
|
||||
if tile.window() == window {
|
||||
tile.start_open_animation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MonitorSet::NoOutputs { workspaces, .. } => {
|
||||
for ws in workspaces {
|
||||
if ws.has_window(window) {
|
||||
for col in &mut ws.columns {
|
||||
for tile in &mut col.tiles {
|
||||
if tile.window() == window {
|
||||
tile.start_open_animation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout<Window> {
|
||||
@@ -1508,10 +1690,7 @@ mod tests {
|
||||
|
||||
impl<W: LayoutElement> Default for Layout<W> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
monitor_set: MonitorSet::NoOutputs { workspaces: vec![] },
|
||||
options: Rc::new(Options::default()),
|
||||
}
|
||||
Self::with_options(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1523,6 +1702,7 @@ mod tests {
|
||||
requested_size: Cell<Option<Size<i32, Logical>>>,
|
||||
min_size: Size<i32, Logical>,
|
||||
max_size: Size<i32, Logical>,
|
||||
pending_fullscreen: Cell<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -1542,6 +1722,7 @@ mod tests {
|
||||
requested_size: Cell::new(None),
|
||||
min_size,
|
||||
max_size,
|
||||
pending_fullscreen: Cell::new(false),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1587,20 +1768,23 @@ mod tests {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: Renderer + ImportAll>(
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_location: Point<i32, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
) -> Vec<WorkspaceRenderElement<R>> {
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>) {
|
||||
self.0.requested_size.set(Some(size));
|
||||
self.0.pending_fullscreen.set(false);
|
||||
}
|
||||
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {}
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||
self.0.pending_fullscreen.set(true);
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
self.0.min_size
|
||||
@@ -1624,9 +1808,15 @@ mod tests {
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
self.0.pending_fullscreen.get()
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
|
||||
@@ -1677,6 +1867,16 @@ mod tests {
|
||||
#[proptest(strategy = "arbitrary_min_max_size()")]
|
||||
min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
|
||||
},
|
||||
AddWindowRightOf {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
id: usize,
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
right_of_id: usize,
|
||||
#[proptest(strategy = "arbitrary_bbox()")]
|
||||
bbox: Rectangle<i32, Logical>,
|
||||
#[proptest(strategy = "arbitrary_min_max_size()")]
|
||||
min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
|
||||
},
|
||||
CloseWindow(#[proptest(strategy = "1..=5usize")] usize),
|
||||
FullscreenWindow(#[proptest(strategy = "1..=5usize")] usize),
|
||||
FocusColumnLeft,
|
||||
@@ -1695,6 +1895,8 @@ mod tests {
|
||||
MoveWindowUp,
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
ConsumeOrExpelWindowLeft,
|
||||
ConsumeOrExpelWindowRight,
|
||||
ConsumeWindowIntoColumn,
|
||||
ExpelWindowFromColumn,
|
||||
CenterColumn,
|
||||
@@ -1716,6 +1918,7 @@ mod tests {
|
||||
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
|
||||
SetWindowHeight(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
|
||||
Communicate(#[proptest(strategy = "1..=5usize")] usize),
|
||||
MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8),
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -1739,7 +1942,7 @@ mod tests {
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: Size::from((1280, 720)),
|
||||
refresh: 60,
|
||||
refresh: 60000,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
@@ -1794,6 +1997,58 @@ mod tests {
|
||||
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
|
||||
layout.add_window(win, None, false);
|
||||
}
|
||||
Op::AddWindowRightOf {
|
||||
id,
|
||||
right_of_id,
|
||||
bbox,
|
||||
min_max_size,
|
||||
} => {
|
||||
let mut found_right_of = false;
|
||||
|
||||
match &mut layout.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
for mon in monitors {
|
||||
for ws in &mut mon.workspaces {
|
||||
for win in ws.windows() {
|
||||
if win.0.id == id {
|
||||
return;
|
||||
}
|
||||
|
||||
if win.0.id == right_of_id {
|
||||
found_right_of = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MonitorSet::NoOutputs { workspaces, .. } => {
|
||||
for ws in workspaces {
|
||||
for win in ws.windows() {
|
||||
if win.0.id == id {
|
||||
return;
|
||||
}
|
||||
|
||||
if win.0.id == right_of_id {
|
||||
found_right_of = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_right_of {
|
||||
return;
|
||||
}
|
||||
|
||||
let right_of_win = TestWindow::new(
|
||||
right_of_id,
|
||||
Rectangle::default(),
|
||||
Size::default(),
|
||||
Size::default(),
|
||||
);
|
||||
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
|
||||
layout.add_window_right_of(&right_of_win, win, None, false);
|
||||
}
|
||||
Op::CloseWindow(id) => {
|
||||
let dummy =
|
||||
TestWindow::new(id, Rectangle::default(), Size::default(), Size::default());
|
||||
@@ -1820,6 +2075,8 @@ mod tests {
|
||||
Op::MoveWindowUp => layout.move_up(),
|
||||
Op::MoveWindowDownOrToWorkspaceDown => layout.move_down_or_to_workspace_down(),
|
||||
Op::MoveWindowUpOrToWorkspaceUp => layout.move_up_or_to_workspace_up(),
|
||||
Op::ConsumeOrExpelWindowLeft => layout.consume_or_expel_window_left(),
|
||||
Op::ConsumeOrExpelWindowRight => layout.consume_or_expel_window_right(),
|
||||
Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
|
||||
Op::ExpelWindowFromColumn => layout.expel_from_column(),
|
||||
Op::CenterColumn => layout.center_column(),
|
||||
@@ -1889,6 +2146,14 @@ mod tests {
|
||||
layout.update_window(&win);
|
||||
}
|
||||
}
|
||||
Op::MoveWorkspaceToOutput(id) => {
|
||||
let name = format!("output{id}");
|
||||
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
layout.move_workspace_to_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1904,10 +2169,7 @@ mod tests {
|
||||
|
||||
#[track_caller]
|
||||
fn check_ops_with_options(options: Options, ops: &[Op]) {
|
||||
let mut layout = Layout {
|
||||
options: Rc::new(options),
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = Layout::with_options(options);
|
||||
|
||||
for op in ops {
|
||||
op.apply(&mut layout);
|
||||
@@ -1942,9 +2204,24 @@ mod tests {
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindowRightOf {
|
||||
id: 3,
|
||||
right_of_id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindowRightOf {
|
||||
id: 4,
|
||||
right_of_id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::CloseWindow(0),
|
||||
Op::CloseWindow(1),
|
||||
Op::CloseWindow(2),
|
||||
Op::FullscreenWindow(1),
|
||||
Op::FullscreenWindow(2),
|
||||
Op::FullscreenWindow(3),
|
||||
Op::FocusColumnLeft,
|
||||
Op::FocusColumnRight,
|
||||
Op::FocusWindowUp,
|
||||
@@ -1975,6 +2252,9 @@ mod tests {
|
||||
Op::MoveWindowDownOrToWorkspaceDown,
|
||||
Op::MoveWindowUp,
|
||||
Op::MoveWindowUpOrToWorkspaceUp,
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::ConsumeOrExpelWindowRight,
|
||||
Op::MoveWorkspaceToOutput(1),
|
||||
];
|
||||
|
||||
for third in every_op {
|
||||
@@ -2069,9 +2349,24 @@ mod tests {
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindowRightOf {
|
||||
id: 6,
|
||||
right_of_id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindowRightOf {
|
||||
id: 7,
|
||||
right_of_id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::CloseWindow(0),
|
||||
Op::CloseWindow(1),
|
||||
Op::CloseWindow(2),
|
||||
Op::FullscreenWindow(1),
|
||||
Op::FullscreenWindow(2),
|
||||
Op::FullscreenWindow(3),
|
||||
Op::FocusColumnLeft,
|
||||
Op::FocusColumnRight,
|
||||
Op::FocusWindowUp,
|
||||
@@ -2102,6 +2397,8 @@ mod tests {
|
||||
Op::MoveWindowDownOrToWorkspaceDown,
|
||||
Op::MoveWindowUp,
|
||||
Op::MoveWindowUpOrToWorkspaceUp,
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::ConsumeOrExpelWindowRight,
|
||||
];
|
||||
|
||||
for third in every_op {
|
||||
@@ -2389,6 +2686,100 @@ mod tests {
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_workspace_to_output() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddOutput(2),
|
||||
Op::FocusOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::MoveWorkspaceToOutput(2),
|
||||
];
|
||||
|
||||
let mut layout = Layout::default();
|
||||
for op in ops {
|
||||
op.apply(&mut layout);
|
||||
}
|
||||
|
||||
let MonitorSet::Normal {
|
||||
monitors,
|
||||
active_monitor_idx,
|
||||
..
|
||||
} = layout.monitor_set
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
assert_eq!(active_monitor_idx, 1);
|
||||
assert_eq!(monitors[0].workspaces.len(), 1);
|
||||
assert!(!monitors[0].workspaces[0].has_windows());
|
||||
assert_eq!(monitors[1].active_workspace_idx, 0);
|
||||
assert_eq!(monitors[1].workspaces.len(), 2);
|
||||
assert!(monitors[1].workspaces[0].has_windows());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||
},
|
||||
Op::FullscreenWindow(1),
|
||||
];
|
||||
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_right_of_on_different_workspace() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||
},
|
||||
Op::FocusWorkspaceDown,
|
||||
Op::AddWindow {
|
||||
id: 2,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||
},
|
||||
Op::AddWindowRightOf {
|
||||
id: 3,
|
||||
right_of_id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||
},
|
||||
];
|
||||
|
||||
let mut layout = Layout::default();
|
||||
for op in ops {
|
||||
op.apply(&mut layout);
|
||||
}
|
||||
|
||||
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mon = monitors.into_iter().next().unwrap();
|
||||
assert_eq!(
|
||||
mon.active_workspace_idx, 1,
|
||||
"the second workspace must remain active"
|
||||
);
|
||||
assert_eq!(
|
||||
mon.workspaces[0].active_column_idx, 1,
|
||||
"the new window must become active"
|
||||
);
|
||||
}
|
||||
|
||||
fn arbitrary_spacing() -> impl Strategy<Value = u16> {
|
||||
// Give equal weight to:
|
||||
// - 0: the element is disabled
|
||||
@@ -2433,12 +2824,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
fn arbitrary_border()(
|
||||
off in any::<bool>(),
|
||||
width in arbitrary_spacing(),
|
||||
) -> niri_config::Border {
|
||||
niri_config::Border {
|
||||
off,
|
||||
width,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
fn arbitrary_options()(
|
||||
gaps in arbitrary_spacing(),
|
||||
struts in arbitrary_struts(),
|
||||
focus_ring in arbitrary_focus_ring(),
|
||||
border in arbitrary_focus_ring(),
|
||||
border in arbitrary_border(),
|
||||
center_focused_column in arbitrary_center_focused_column(),
|
||||
) -> Options {
|
||||
Options {
|
||||
|
||||
+45
-13
@@ -2,12 +2,10 @@ use std::cmp::min;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::SizeChange;
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
CropRenderElement, Relocate, RelocateRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use smithay::desktop::Window;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||
|
||||
@@ -16,6 +14,7 @@ use super::workspace::{
|
||||
};
|
||||
use super::{LayoutElement, Options};
|
||||
use crate::animation::Animation;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::output_size;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -97,7 +96,8 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||
current_idx,
|
||||
idx as f64,
|
||||
Duration::from_millis(250),
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -127,6 +127,26 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &W,
|
||||
window: W,
|
||||
width: ColumnWidth,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
let workspace_idx = self
|
||||
.workspaces
|
||||
.iter_mut()
|
||||
.position(|ws| ws.has_window(right_of))
|
||||
.unwrap();
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_window_right_of(right_of, window, width, is_full_width);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
@@ -216,6 +236,14 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_left(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_left();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_right(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_right();
|
||||
}
|
||||
|
||||
pub fn focus_left(&mut self) {
|
||||
self.active_workspace().focus_left();
|
||||
}
|
||||
@@ -578,16 +606,11 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.render_above_top_layer()
|
||||
}
|
||||
}
|
||||
|
||||
impl Monitor<Window> {
|
||||
pub fn render_elements<R: Renderer + ImportAll>(
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> Vec<MonitorRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
) -> Vec<MonitorRenderElement<R>> {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
|
||||
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
|
||||
@@ -606,12 +629,18 @@ impl Monitor<Window> {
|
||||
let before = self.workspaces[before_idx].render_elements(renderer);
|
||||
let after = self.workspaces[after_idx].render_elements(renderer);
|
||||
|
||||
// HACK: crop to infinite bounds for all sides except the side where the workspaces
|
||||
// join, to decrease the chance of cutting a lower-scale surface in the middle of a
|
||||
// pixel, thereby disabling its nearest-neighbor upscaling.
|
||||
let before = before.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
output_scale,
|
||||
Rectangle::from_extemities((0, offset), (size.w, size.h)),
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX / 2, size.h),
|
||||
),
|
||||
)?,
|
||||
(0, -offset),
|
||||
Relocate::Relative,
|
||||
@@ -622,7 +651,10 @@ impl Monitor<Window> {
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
output_scale,
|
||||
Rectangle::from_extemities((0, 0), (size.w, offset)),
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, 0),
|
||||
(i32::MAX / 2, i32::MAX / 2),
|
||||
),
|
||||
)?,
|
||||
(0, -offset + size.h),
|
||||
Relocate::Relative,
|
||||
|
||||
+150
-34
@@ -3,14 +3,18 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Element, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::focus_ring::FocusRing;
|
||||
use super::workspace::WorkspaceRenderElement;
|
||||
use super::{LayoutElement, Options};
|
||||
use super::{LayoutElement, LayoutElementRenderElement, Options};
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::offscreen::OffscreenRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
/// Toplevel window with decorations.
|
||||
#[derive(Debug)]
|
||||
@@ -21,6 +25,12 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The border around the window.
|
||||
border: FocusRing,
|
||||
|
||||
/// The focus ring around the window.
|
||||
///
|
||||
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
|
||||
/// currently here.
|
||||
focus_ring: FocusRing,
|
||||
|
||||
/// Whether this tile is fullscreen.
|
||||
///
|
||||
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||
@@ -33,24 +43,38 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The size we were requested to fullscreen into.
|
||||
fullscreen_size: Size<i32, Logical>,
|
||||
|
||||
/// The animation upon opening a window.
|
||||
open_animation: Option<Animation>,
|
||||
|
||||
/// Configurable properties of the layout.
|
||||
options: Rc<Options>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
TileRenderElement => {
|
||||
LayoutElement = LayoutElementRenderElement<R>,
|
||||
SolidColor = RelocateRenderElement<SolidColorRenderElement>,
|
||||
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Tile<W> {
|
||||
pub fn new(window: W, options: Rc<Options>) -> Self {
|
||||
Self {
|
||||
window,
|
||||
border: FocusRing::new(options.border),
|
||||
border: FocusRing::new(options.border.into()),
|
||||
focus_ring: FocusRing::new(options.focus_ring),
|
||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
||||
fullscreen_size: Default::default(),
|
||||
open_animation: None,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||
self.border.update_config(options.border);
|
||||
self.border.update_config(options.border.into());
|
||||
self.focus_ring.update_config(options.focus_ring);
|
||||
self.options = options;
|
||||
}
|
||||
|
||||
@@ -61,7 +85,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
|
||||
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||
let width = self.border.width();
|
||||
self.border.update(
|
||||
(width, width).into(),
|
||||
@@ -69,6 +93,33 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.window.has_ssd(),
|
||||
);
|
||||
self.border.set_active(is_active);
|
||||
|
||||
self.focus_ring
|
||||
.update((0, 0).into(), self.tile_size(), self.has_ssd());
|
||||
self.focus_ring.set_active(is_active);
|
||||
|
||||
match &mut self.open_animation {
|
||||
Some(anim) => {
|
||||
anim.set_current_time(current_time);
|
||||
if anim.is_done() {
|
||||
self.open_animation = None;
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.open_animation.is_some()
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self) {
|
||||
self.open_animation = Some(Animation::new(
|
||||
0.,
|
||||
1.,
|
||||
self.options.animations.window_open,
|
||||
niri_config::Animation::default_window_open(),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &W {
|
||||
@@ -141,6 +192,22 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.window.size()
|
||||
}
|
||||
|
||||
/// Returns an animated size of the tile for rendering and input.
|
||||
///
|
||||
/// During the window opening animation, windows to the right should gradually slide further to
|
||||
/// the right. This is what this visual size is used for. Other things like window resizes or
|
||||
/// transactions or new view position calculation always use the real size, instead of this
|
||||
/// visual size.
|
||||
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
|
||||
let size = self.tile_size();
|
||||
let v = self
|
||||
.open_animation
|
||||
.as_ref()
|
||||
.map(|anim| anim.value())
|
||||
.unwrap_or(1.);
|
||||
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
|
||||
}
|
||||
|
||||
pub fn buf_loc(&self) -> Point<i32, Logical> {
|
||||
let mut loc = Point::from((0, 0));
|
||||
loc += self.window_loc();
|
||||
@@ -232,36 +299,44 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.effective_border_width().is_some() || self.window.has_ssd()
|
||||
}
|
||||
|
||||
pub fn render<R: Renderer + ImportAll>(
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> Vec<WorkspaceRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
let mut rv = Vec::new();
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
let rv = self
|
||||
.window
|
||||
.render(renderer, location + self.window_loc(), scale)
|
||||
.into_iter()
|
||||
.map(Into::into);
|
||||
|
||||
let window_pos = location + self.window_loc();
|
||||
rv.extend(self.window.render(renderer, window_pos, scale));
|
||||
let elem = self.effective_border_width().map(|_| {
|
||||
self.border.render(scale).map(move |elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
if self.effective_border_width().is_some() {
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(scale)
|
||||
.map(|elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
.map(Into::into),
|
||||
);
|
||||
}
|
||||
let elem = focus_ring.then(|| {
|
||||
self.focus_ring.render(scale).map(move |elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
if self.is_fullscreen {
|
||||
let elem = self.is_fullscreen.then(|| {
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&self.fullscreen_backdrop,
|
||||
location.to_physical_precise_round(scale),
|
||||
@@ -269,9 +344,50 @@ impl<W: LayoutElement> Tile<W> {
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.push(elem.into());
|
||||
}
|
||||
RelocateRenderElement::from_element(elem, (0, 0), Relocate::Relative).into()
|
||||
});
|
||||
rv.chain(elem)
|
||||
}
|
||||
|
||||
rv
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
if let Some(anim) = &self.open_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, location, scale, focus_ring);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
|
||||
let elem = OffscreenRenderElement::new(
|
||||
renderer,
|
||||
scale.x as i32,
|
||||
&elements,
|
||||
anim.value() as f32,
|
||||
);
|
||||
self.window()
|
||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||
|
||||
let mut center = location;
|
||||
center.x += self.tile_size().w / 2;
|
||||
center.y += self.tile_size().h / 2;
|
||||
|
||||
Some(TileRenderElement::Offscreen(
|
||||
RescaleRenderElement::from_element(
|
||||
elem,
|
||||
center.to_physical_precise_round(scale),
|
||||
(anim.value() / 2. + 0.5).min(1.),
|
||||
),
|
||||
))
|
||||
.into_iter()
|
||||
.chain(None.into_iter().flatten())
|
||||
} else {
|
||||
self.window().set_offscreen_element_id(None);
|
||||
|
||||
let elements = self.render_inner(renderer, location, scale, focus_ring);
|
||||
None.into_iter().chain(Some(elements).into_iter().flatten())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+247
-140
@@ -1,23 +1,21 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::iter::zip;
|
||||
use std::iter::{self, zip};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, PresetWidth, SizeChange, Struts};
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::utils::RelocateRenderElement;
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::desktop::space::SpaceElement;
|
||||
use smithay::desktop::{layer_map_for_output, Window};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::render_elements;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use super::tile::Tile;
|
||||
use super::tile::{Tile, TileRenderElement};
|
||||
use super::{LayoutElement, Options};
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::output_size;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -49,9 +47,6 @@ pub struct Workspace<W: LayoutElement> {
|
||||
/// Index of the currently active column, if any.
|
||||
pub active_column_idx: usize,
|
||||
|
||||
/// Focus ring buffer and parameters.
|
||||
focus_ring: FocusRing,
|
||||
|
||||
/// Offset of the view computed from the active column.
|
||||
///
|
||||
/// Any gaps, including left padding from work area left exclusive zone, is handled
|
||||
@@ -79,12 +74,10 @@ pub struct Workspace<W: LayoutElement> {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutputId(String);
|
||||
|
||||
render_elements! {
|
||||
#[derive(Debug)]
|
||||
pub WorkspaceRenderElement<R> where R: ImportAll;
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
FocusRing = FocusRingRenderElement,
|
||||
Border = RelocateRenderElement<FocusRingRenderElement>,
|
||||
niri_render_elements! {
|
||||
WorkspaceRenderElement => {
|
||||
Tile = TileRenderElement<R>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of a column.
|
||||
@@ -197,7 +190,6 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
output: Some(output),
|
||||
columns: vec![],
|
||||
active_column_idx: 0,
|
||||
focus_ring: FocusRing::new(options.focus_ring),
|
||||
view_offset: 0,
|
||||
view_offset_anim: None,
|
||||
activate_prev_column_on_removal: false,
|
||||
@@ -213,7 +205,6 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)),
|
||||
columns: vec![],
|
||||
active_column_idx: 0,
|
||||
focus_ring: FocusRing::new(options.focus_ring),
|
||||
view_offset: 0,
|
||||
view_offset_anim: None,
|
||||
activate_prev_column_on_removal: false,
|
||||
@@ -233,42 +224,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
None => (),
|
||||
}
|
||||
|
||||
let view_pos = self.view_pos();
|
||||
|
||||
for (col_idx, col) in self.columns.iter_mut().enumerate() {
|
||||
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
|
||||
let is_active = is_active
|
||||
&& col_idx == self.active_column_idx
|
||||
&& tile_idx == col.active_tile_idx;
|
||||
tile.advance_animations(current_time, is_active);
|
||||
}
|
||||
}
|
||||
|
||||
// This shall one day become a proper animation.
|
||||
if !self.columns.is_empty() {
|
||||
let col = &self.columns[self.active_column_idx];
|
||||
let active_tile = &col.tiles[col.active_tile_idx];
|
||||
let size = active_tile.tile_size();
|
||||
let has_ssd = active_tile.has_ssd();
|
||||
|
||||
let tile_pos = Point::from((
|
||||
self.column_x(self.active_column_idx) - view_pos,
|
||||
col.tile_y(col.active_tile_idx),
|
||||
));
|
||||
|
||||
self.focus_ring.update(tile_pos, size, has_ssd);
|
||||
self.focus_ring.set_active(is_active);
|
||||
let is_active = is_active && col_idx == self.active_column_idx;
|
||||
col.advance_animations(current_time, is_active);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.view_offset_anim.is_some()
|
||||
self.view_offset_anim.is_some() || self.columns.iter().any(Column::are_animations_ongoing)
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||
self.focus_ring.update_config(options.focus_ring);
|
||||
// The focus ring buffer will be updated in a subsequent update_animations call.
|
||||
|
||||
for column in &mut self.columns {
|
||||
column.update_config(options.clone());
|
||||
}
|
||||
@@ -330,6 +296,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_size(&self) -> Size<i32, Logical> {
|
||||
self.view_size
|
||||
}
|
||||
|
||||
pub fn update_output_scale_transform(&mut self) {
|
||||
let Some(output) = self.output.as_ref() else {
|
||||
return;
|
||||
@@ -351,7 +321,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn configure_new_window(&self, window: &Window) {
|
||||
pub fn new_window_size(&self) -> Size<i32, Logical> {
|
||||
let width = if let Some(width) = self.options.default_width {
|
||||
let mut width = width.resolve(&self.options, self.working_area.size.w);
|
||||
if !self.options.border.off {
|
||||
@@ -367,8 +337,11 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
height -= self.options.border.width as i32 * 2;
|
||||
}
|
||||
|
||||
let size = Size::from((width, max(height, 1)));
|
||||
Size::from((width, max(height, 1)))
|
||||
}
|
||||
|
||||
pub fn configure_new_window(&self, window: &Window) {
|
||||
let size = self.new_window_size();
|
||||
let bounds = self.toplevel_bounds();
|
||||
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
@@ -429,7 +402,8 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.view_offset_anim = Some(Animation::new(
|
||||
self.view_offset as f64,
|
||||
new_view_offset as f64,
|
||||
Duration::from_millis(250),
|
||||
self.options.animations.horizontal_view_movement,
|
||||
niri_config::Animation::default_horizontal_view_movement(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -535,6 +509,16 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
x
|
||||
}
|
||||
|
||||
fn visual_column_x(&self, column_idx: usize) -> i32 {
|
||||
let mut x = 0;
|
||||
|
||||
for column in self.columns.iter().take(column_idx) {
|
||||
x += column.visual_width() + self.options.gaps;
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
pub fn add_window(
|
||||
&mut self,
|
||||
window: W,
|
||||
@@ -583,6 +567,41 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &W,
|
||||
window: W,
|
||||
width: ColumnWidth,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
self.enter_output_for_window(&window);
|
||||
|
||||
let right_of_idx = self
|
||||
.columns
|
||||
.iter()
|
||||
.position(|col| col.contains(right_of))
|
||||
.unwrap();
|
||||
let idx = right_of_idx + 1;
|
||||
|
||||
let column = Column::new(
|
||||
window,
|
||||
self.view_size,
|
||||
self.working_area,
|
||||
self.options.clone(),
|
||||
width,
|
||||
is_full_width,
|
||||
);
|
||||
self.columns.insert(idx, column);
|
||||
|
||||
// Activate the new window if right_of was active.
|
||||
if self.active_column_idx == right_of_idx {
|
||||
self.activate_column(idx);
|
||||
self.activate_prev_column_on_removal = true;
|
||||
} else if idx <= self.active_column_idx {
|
||||
self.active_column_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, mut column: Column<W>, activate: bool) {
|
||||
for tile in &column.tiles {
|
||||
self.enter_output_for_window(tile.window());
|
||||
@@ -862,6 +881,70 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.columns[self.active_column_idx].move_up();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_left(&mut self) {
|
||||
if self.columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let source_column = &self.columns[self.active_column_idx];
|
||||
if source_column.tiles.len() == 1 {
|
||||
if self.active_column_idx == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move into adjacent column.
|
||||
let target_column_idx = self.active_column_idx - 1;
|
||||
let window = self.remove_window_by_idx(self.active_column_idx, 0);
|
||||
self.enter_output_for_window(&window);
|
||||
|
||||
let target_column = &mut self.columns[target_column_idx];
|
||||
target_column.add_window(window);
|
||||
target_column.focus_last();
|
||||
self.activate_column(target_column_idx);
|
||||
} else {
|
||||
// Move out of column.
|
||||
let width = source_column.width;
|
||||
let is_full_width = source_column.is_full_width;
|
||||
let window =
|
||||
self.remove_window_by_idx(self.active_column_idx, source_column.active_tile_idx);
|
||||
|
||||
self.add_window(window, true, width, is_full_width);
|
||||
// Window was added to the right of current column, so move the new column left.
|
||||
self.move_left();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_right(&mut self) {
|
||||
if self.columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let source_column = &self.columns[self.active_column_idx];
|
||||
if source_column.tiles.len() == 1 {
|
||||
if self.active_column_idx + 1 == self.columns.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move into adjacent column.
|
||||
let target_column_idx = self.active_column_idx;
|
||||
let window = self.remove_window_by_idx(self.active_column_idx, 0);
|
||||
self.enter_output_for_window(&window);
|
||||
|
||||
let target_column = &mut self.columns[target_column_idx];
|
||||
target_column.add_window(window);
|
||||
target_column.focus_last();
|
||||
self.activate_column(target_column_idx);
|
||||
} else {
|
||||
// Move out of column.
|
||||
let width = source_column.width;
|
||||
let is_full_width = source_column.is_full_width;
|
||||
let window =
|
||||
self.remove_window_by_idx(self.active_column_idx, source_column.active_tile_idx);
|
||||
|
||||
self.add_window(window, true, width, is_full_width);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_into_column(&mut self) {
|
||||
if self.columns.len() < 2 {
|
||||
return;
|
||||
@@ -906,6 +989,45 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.column_x(self.active_column_idx) + self.view_offset
|
||||
}
|
||||
|
||||
fn tiles_in_render_order(&self) -> impl Iterator<Item = (&'_ Tile<W>, Point<i32, Logical>)> {
|
||||
let view_pos = self.visual_column_x(self.active_column_idx) + self.view_offset;
|
||||
|
||||
// Start with the active window since it's drawn on top.
|
||||
let col = &self.columns[self.active_column_idx];
|
||||
let tile = &col.tiles[col.active_tile_idx];
|
||||
let tile_pos = Point::from((
|
||||
self.visual_column_x(self.active_column_idx) - view_pos,
|
||||
col.tile_y(col.active_tile_idx),
|
||||
));
|
||||
let first = iter::once((tile, tile_pos));
|
||||
|
||||
let mut x = -view_pos;
|
||||
let rest = self
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
// Keep track of column X position.
|
||||
.map(move |(col_idx, col)| {
|
||||
let rv = (col_idx, col, x);
|
||||
x += col.visual_width() + self.options.gaps;
|
||||
rv
|
||||
})
|
||||
.flat_map(move |(col_idx, col, x)| {
|
||||
zip(&col.tiles, col.tile_ys()).enumerate().filter_map(
|
||||
move |(tile_idx, (tile, y))| {
|
||||
if col_idx == self.active_column_idx && tile_idx == col.active_tile_idx {
|
||||
// Active tile comes first.
|
||||
return None;
|
||||
}
|
||||
|
||||
let tile_pos = Point::from((x, y));
|
||||
Some((tile, tile_pos))
|
||||
},
|
||||
)
|
||||
});
|
||||
first.chain(rest)
|
||||
}
|
||||
|
||||
pub fn window_under(
|
||||
&self,
|
||||
pos: Point<f64, Logical>,
|
||||
@@ -914,45 +1036,18 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let view_pos = self.view_pos();
|
||||
self.tiles_in_render_order().find_map(|(tile, tile_pos)| {
|
||||
let pos_within_tile = pos - tile_pos.to_f64();
|
||||
|
||||
// Prefer the active window since it's drawn on top.
|
||||
let col = &self.columns[self.active_column_idx];
|
||||
let active_tile = &col.tiles[col.active_tile_idx];
|
||||
let tile_pos = Point::from((
|
||||
self.column_x(self.active_column_idx) - view_pos,
|
||||
col.tile_y(col.active_tile_idx),
|
||||
));
|
||||
let pos_within_tile = pos - tile_pos.to_f64();
|
||||
if active_tile.is_in_input_region(pos_within_tile) {
|
||||
let pos_within_surface = tile_pos + active_tile.buf_loc();
|
||||
return Some((active_tile.window(), Some(pos_within_surface)));
|
||||
} else if active_tile.is_in_activation_region(pos_within_tile) {
|
||||
return Some((active_tile.window(), None));
|
||||
}
|
||||
|
||||
let mut x = -view_pos;
|
||||
for col in &self.columns {
|
||||
for (tile, y) in zip(&col.tiles, col.tile_ys()) {
|
||||
if tile.window() == active_tile.window() {
|
||||
// Already handled it above.
|
||||
continue;
|
||||
}
|
||||
|
||||
let tile_pos = Point::from((x, y));
|
||||
let pos_within_tile = pos - tile_pos.to_f64();
|
||||
if tile.is_in_input_region(pos_within_tile) {
|
||||
let pos_within_surface = tile_pos + tile.buf_loc();
|
||||
return Some((tile.window(), Some(pos_within_surface)));
|
||||
} else if tile.is_in_activation_region(pos_within_tile) {
|
||||
return Some((tile.window(), None));
|
||||
}
|
||||
if tile.is_in_input_region(pos_within_tile) {
|
||||
let pos_within_surface = tile_pos + tile.buf_loc();
|
||||
return Some((tile.window(), Some(pos_within_surface)));
|
||||
} else if tile.is_in_activation_region(pos_within_tile) {
|
||||
return Some((tile.window(), None));
|
||||
}
|
||||
|
||||
x += col.width() + self.options.gaps;
|
||||
}
|
||||
|
||||
None
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_width(&mut self) {
|
||||
@@ -1051,6 +1146,39 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
self.columns[self.active_column_idx].is_fullscreen
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> Vec<WorkspaceRenderElement<R>> {
|
||||
if self.columns.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// FIXME: workspaces should probably cache their last used scale so they can be correctly
|
||||
// rendered even with no outputs connected.
|
||||
let output_scale = self
|
||||
.output
|
||||
.as_ref()
|
||||
.map(|o| Scale::from(o.current_scale().fractional_scale()))
|
||||
.unwrap_or(Scale::from(1.));
|
||||
|
||||
let mut rv = vec![];
|
||||
let mut first = true;
|
||||
|
||||
for (tile, tile_pos) in self.tiles_in_render_order() {
|
||||
// For the active tile (which comes first), draw the focus ring.
|
||||
let focus_ring = first;
|
||||
first = false;
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, output_scale, focus_ring)
|
||||
.map(Into::into),
|
||||
);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl Workspace<Window> {
|
||||
@@ -1074,60 +1202,6 @@ impl Workspace<Window> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_elements<R: Renderer + ImportAll>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> Vec<WorkspaceRenderElement<R>>
|
||||
where
|
||||
<R as Renderer>::TextureId: 'static,
|
||||
{
|
||||
if self.columns.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// FIXME: workspaces should probably cache their last used scale so they can be correctly
|
||||
// rendered even with no outputs connected.
|
||||
let output_scale = self
|
||||
.output
|
||||
.as_ref()
|
||||
.map(|o| Scale::from(o.current_scale().fractional_scale()))
|
||||
.unwrap_or(Scale::from(1.));
|
||||
|
||||
let mut rv = vec![];
|
||||
let view_pos = self.view_pos();
|
||||
|
||||
// Draw the active window on top.
|
||||
let col = &self.columns[self.active_column_idx];
|
||||
let active_tile = &col.tiles[col.active_tile_idx];
|
||||
let tile_pos = Point::from((
|
||||
self.column_x(self.active_column_idx) - view_pos,
|
||||
col.tile_y(col.active_tile_idx),
|
||||
));
|
||||
|
||||
// Draw the window itself.
|
||||
rv.extend(active_tile.render(renderer, tile_pos, output_scale));
|
||||
|
||||
// Draw the focus ring.
|
||||
rv.extend(self.focus_ring.render(output_scale).map(Into::into));
|
||||
|
||||
let mut x = -view_pos;
|
||||
for col in &self.columns {
|
||||
for (tile, y) in zip(&col.tiles, col.tile_ys()) {
|
||||
if tile.window() == active_tile.window() {
|
||||
// Already handled it above.
|
||||
continue;
|
||||
}
|
||||
|
||||
let tile_pos = Point::from((x, y));
|
||||
rv.extend(tile.render(renderer, tile_pos, output_scale));
|
||||
}
|
||||
|
||||
x += col.width() + self.options.gaps;
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Column<W> {
|
||||
@@ -1151,8 +1225,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
options,
|
||||
};
|
||||
|
||||
let is_pending_fullscreen = window.is_pending_fullscreen();
|
||||
|
||||
rv.add_window(window);
|
||||
|
||||
if is_pending_fullscreen {
|
||||
rv.set_fullscreen(true);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
@@ -1204,6 +1284,17 @@ impl<W: LayoutElement> Column<W> {
|
||||
self.update_tile_sizes();
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||
for (tile_idx, tile) in self.tiles.iter_mut().enumerate() {
|
||||
let is_active = is_active && tile_idx == self.active_tile_idx;
|
||||
tile.advance_animations(current_time, is_active);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.tiles.iter().any(Tile::are_animations_ongoing)
|
||||
}
|
||||
|
||||
pub fn contains(&self, window: &W) -> bool {
|
||||
self.tiles.iter().map(Tile::window).any(|win| win == window)
|
||||
}
|
||||
@@ -1402,6 +1493,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn visual_width(&self) -> i32 {
|
||||
self.tiles
|
||||
.iter()
|
||||
.map(|tile| tile.visual_tile_size().w)
|
||||
.max()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn focus_up(&mut self) {
|
||||
self.active_tile_idx = self.active_tile_idx.saturating_sub(1);
|
||||
}
|
||||
@@ -1410,6 +1509,10 @@ impl<W: LayoutElement> Column<W> {
|
||||
self.active_tile_idx = min(self.active_tile_idx + 1, self.tiles.len() - 1);
|
||||
}
|
||||
|
||||
fn focus_last(&mut self) {
|
||||
self.active_tile_idx = self.tiles.len() - 1;
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
let new_idx = self.active_tile_idx.saturating_sub(1);
|
||||
if self.active_tile_idx == new_idx {
|
||||
@@ -1441,6 +1544,10 @@ impl<W: LayoutElement> Column<W> {
|
||||
if self.is_fullscreen {
|
||||
assert_eq!(self.tiles.len(), 1);
|
||||
}
|
||||
|
||||
for tile in &self.tiles {
|
||||
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_width(&mut self) {
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
pub mod animation;
|
||||
pub mod backend;
|
||||
pub mod cli;
|
||||
pub mod config_error_notification;
|
||||
pub mod cursor;
|
||||
#[cfg(feature = "dbus")]
|
||||
pub mod dbus;
|
||||
pub mod exit_confirm_dialog;
|
||||
pub mod frame_clock;
|
||||
pub mod handlers;
|
||||
pub mod hotkey_overlay;
|
||||
pub mod input;
|
||||
pub mod ipc;
|
||||
pub mod layout;
|
||||
pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod screenshot_ui;
|
||||
pub mod utils;
|
||||
pub mod watcher;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
+70
-93
@@ -1,95 +1,30 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
mod animation;
|
||||
mod backend;
|
||||
mod config_error_notification;
|
||||
mod cursor;
|
||||
#[cfg(feature = "dbus")]
|
||||
mod dbus;
|
||||
mod exit_confirm_dialog;
|
||||
mod frame_clock;
|
||||
mod handlers;
|
||||
mod hotkey_overlay;
|
||||
mod input;
|
||||
mod ipc;
|
||||
mod layout;
|
||||
mod niri;
|
||||
mod render_helpers;
|
||||
mod screenshot_ui;
|
||||
mod utils;
|
||||
mod watcher;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
mod pw_utils;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::{env, mem};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
use dummy_pw_utils as pw_utils;
|
||||
use git_version::git_version;
|
||||
use niri::{Niri, State};
|
||||
use niri::animation;
|
||||
use niri::cli::{Cli, Sub};
|
||||
#[cfg(feature = "dbus")]
|
||||
use niri::dbus;
|
||||
use niri::ipc::client::handle_msg;
|
||||
use niri::niri::State;
|
||||
use niri::utils::{
|
||||
cause_panic, spawn, version, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||
};
|
||||
use niri::watcher::Watcher;
|
||||
use niri_config::Config;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
use smithay::reexports::calloop::{self, EventLoop};
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use utils::spawn;
|
||||
use watcher::Watcher;
|
||||
|
||||
use crate::ipc::client::handle_msg;
|
||||
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version = version(), about, long_about = None)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||
#[command(subcommand_help_heading = "Subcommands")]
|
||||
struct Cli {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Command to run upon compositor startup.
|
||||
#[arg(last = true)]
|
||||
command: Vec<OsString>,
|
||||
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<Sub>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
msg: Msg,
|
||||
/// Format output as JSON.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Msg {
|
||||
/// List connected outputs.
|
||||
Outputs,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
@@ -154,7 +89,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("starting version {}", &version());
|
||||
|
||||
// Load the config.
|
||||
let path = cli.config.or_else(default_config_path);
|
||||
let mut config_created = false;
|
||||
let path = cli.config.or_else(|| {
|
||||
let default_path = default_config_path()?;
|
||||
let default_parent = default_path.parent().unwrap();
|
||||
|
||||
if let Err(err) = fs::create_dir_all(default_parent) {
|
||||
warn!(
|
||||
"error creating config directories {:?}: {err:?}",
|
||||
default_parent
|
||||
);
|
||||
return Some(default_path);
|
||||
}
|
||||
|
||||
// Create the config and fill it with the default config if it doesn't exist.
|
||||
let new_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&default_path);
|
||||
match new_file {
|
||||
Ok(mut new_file) => {
|
||||
let default = include_bytes!("../resources/default-config.kdl");
|
||||
match new_file.write_all(default) {
|
||||
Ok(()) => {
|
||||
config_created = true;
|
||||
info!("wrote default config to {:?}", &default_path);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error writing config file at {:?}: {err:?}", &default_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
|
||||
}
|
||||
|
||||
Some(default_path)
|
||||
});
|
||||
|
||||
let mut config_errored = false;
|
||||
let mut config = path
|
||||
@@ -169,7 +141,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
||||
let slowdown = if config.animations.off {
|
||||
0.
|
||||
} else {
|
||||
config.animations.slowdown.clamp(0., 100.)
|
||||
};
|
||||
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
|
||||
// Create the compositor.
|
||||
@@ -180,7 +158,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
event_loop.handle(),
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Set WAYLAND_DISPLAY for children.
|
||||
let socket_name = &state.niri.socket_name;
|
||||
@@ -218,7 +197,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
};
|
||||
|
||||
// Set up config file watcher.
|
||||
let _watcher = if let Some(path) = path {
|
||||
let _watcher = if let Some(path) = path.clone() {
|
||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||
let watcher = Watcher::new(path.clone(), tx);
|
||||
event_loop
|
||||
@@ -243,6 +222,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Show the config error notification right away if needed.
|
||||
if config_errored {
|
||||
state.niri.config_error_notification.show();
|
||||
} else if config_created {
|
||||
state.niri.config_error_notification.show_created(path);
|
||||
}
|
||||
|
||||
// Run the compositor.
|
||||
@@ -253,21 +234,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
git_version!(fallback = "unknown commit"),
|
||||
)
|
||||
}
|
||||
|
||||
fn import_env_to_systemd() {
|
||||
let variables = ["WAYLAND_DISPLAY", niri_ipc::SOCKET_PATH_ENV].join(" ");
|
||||
|
||||
let rv = Command::new("/bin/sh")
|
||||
.args([
|
||||
"-c",
|
||||
"systemctl --user import-environment WAYLAND_DISPLAY && \
|
||||
hash dbus-update-activation-environment 2>/dev/null && \
|
||||
dbus-update-activation-environment WAYLAND_DISPLAY",
|
||||
&format!(
|
||||
"systemctl --user import-environment {variables} && \
|
||||
hash dbus-update-activation-environment 2>/dev/null && \
|
||||
dbus-update-activation-environment {variables}"
|
||||
),
|
||||
])
|
||||
.spawn();
|
||||
// Wait for the import process to complete, otherwise services will start too fast without
|
||||
|
||||
+339
-474
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,466 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr;
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
pub struct ForeignToplevelManagerState {
|
||||
display: DisplayHandle,
|
||||
instances: Vec<ZwlrForeignToplevelManagerV1>,
|
||||
toplevels: HashMap<WlSurface, ToplevelData>,
|
||||
}
|
||||
|
||||
pub trait ForeignToplevelHandler {
|
||||
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
|
||||
fn activate(&mut self, wl_surface: WlSurface);
|
||||
fn close(&mut self, wl_surface: WlSurface);
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
|
||||
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
|
||||
}
|
||||
|
||||
struct ToplevelData {
|
||||
title: Option<String>,
|
||||
app_id: Option<String>,
|
||||
states: ArrayVec<u32, 3>,
|
||||
output: Option<Output>,
|
||||
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
// FIXME: parent.
|
||||
}
|
||||
|
||||
pub struct ForeignToplevelGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ForeignToplevelManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = ForeignToplevelGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
|
||||
Self {
|
||||
display: display.clone(),
|
||||
instances: Vec::new(),
|
||||
toplevels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(state: &mut State) {
|
||||
let _span = tracy_client::span!("foreign_toplevel::refresh");
|
||||
|
||||
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||
|
||||
// Handle closed windows.
|
||||
protocol_state.toplevels.retain(|surface, data| {
|
||||
if state.niri.layout.find_window_and_output(surface).is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for instance in data.instances.keys() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
// Handle new and existing windows.
|
||||
//
|
||||
// Save the focused window for last, this way when the focus changes, we will first deactivate
|
||||
// the previous window and only then activate the newly focused window.
|
||||
let mut focused = None;
|
||||
state.niri.layout.with_windows(|window, output| {
|
||||
let wl_surface = window.toplevel().wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
|
||||
focused = Some((window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
let wl_surface = window.toplevel().wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
|
||||
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
|
||||
|
||||
let Some(client) = wl_output.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||
for data in protocol_state.toplevels.values_mut() {
|
||||
if data.output.as_ref() != Some(output) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if instance.client().as_ref() != Some(&client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
instance.output_enter(wl_output);
|
||||
instance.done();
|
||||
outputs.push(wl_output.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_toplevel(
|
||||
protocol_state: &mut ForeignToplevelManagerState,
|
||||
wl_surface: &WlSurface,
|
||||
role: &XdgToplevelSurfaceRoleAttributes,
|
||||
output: Option<&Output>,
|
||||
has_focus: bool,
|
||||
) {
|
||||
let states = to_state_vec(&role.current.states, has_focus);
|
||||
|
||||
match protocol_state.toplevels.entry(wl_surface.clone()) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Existing window, check if anything changed.
|
||||
let data = entry.into_mut();
|
||||
|
||||
let mut new_title = None;
|
||||
if data.title != role.title {
|
||||
data.title = role.title.clone();
|
||||
new_title = role.title.as_deref();
|
||||
|
||||
if new_title.is_none() {
|
||||
error!("toplevel title changed to None");
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_app_id = None;
|
||||
if data.app_id != role.app_id {
|
||||
data.app_id = role.app_id.clone();
|
||||
new_app_id = role.app_id.as_deref();
|
||||
|
||||
if new_app_id.is_none() {
|
||||
error!("toplevel app_id changed to None");
|
||||
}
|
||||
}
|
||||
|
||||
let mut states_changed = false;
|
||||
if data.states != states {
|
||||
data.states = states;
|
||||
states_changed = true;
|
||||
}
|
||||
|
||||
let mut output_changed = false;
|
||||
if data.output.as_ref() != output {
|
||||
data.output = output.cloned();
|
||||
output_changed = true;
|
||||
}
|
||||
|
||||
let something_changed =
|
||||
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
|
||||
|
||||
if something_changed {
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
if let Some(new_app_id) = new_app_id {
|
||||
instance.app_id(new_app_id.to_owned());
|
||||
}
|
||||
if states_changed {
|
||||
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||
}
|
||||
if output_changed {
|
||||
for wl_output in outputs.drain(..) {
|
||||
instance.output_leave(&wl_output);
|
||||
}
|
||||
if let Some(output) = &data.output {
|
||||
if let Some(client) = instance.client() {
|
||||
for wl_output in output.client_outputs(&client) {
|
||||
instance.output_enter(&wl_output);
|
||||
outputs.push(wl_output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instance.done();
|
||||
}
|
||||
}
|
||||
|
||||
for outputs in data.instances.values_mut() {
|
||||
// Clean up dead wl_outputs.
|
||||
outputs.retain(|x| x.is_alive());
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
// New window, start tracking it.
|
||||
let mut data = ToplevelData {
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
states,
|
||||
output: output.cloned(),
|
||||
instances: HashMap::new(),
|
||||
};
|
||||
|
||||
for manager in &protocol_state.instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
entry.insert(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelData {
|
||||
fn add_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: &ZwlrForeignToplevelManagerV1,
|
||||
) where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let toplevel = client
|
||||
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
|
||||
.unwrap();
|
||||
manager.toplevel(&toplevel);
|
||||
|
||||
if let Some(title) = &self.title {
|
||||
toplevel.title(title.clone());
|
||||
}
|
||||
if let Some(app_id) = &self.app_id {
|
||||
toplevel.app_id(app_id.clone());
|
||||
}
|
||||
|
||||
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||
|
||||
let mut outputs = Vec::new();
|
||||
if let Some(output) = &self.output {
|
||||
for wl_output in output.client_outputs(client) {
|
||||
toplevel.output_enter(&wl_output);
|
||||
outputs.push(wl_output);
|
||||
}
|
||||
}
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.instances.insert(toplevel, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
|
||||
for ForeignToplevelManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
resource: New<ZwlrForeignToplevelManagerV1>,
|
||||
_global_data: &ForeignToplevelGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(resource, ());
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.instances.push(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ZwlrForeignToplevelHandleV1,
|
||||
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let protocol_state = state.foreign_toplevel_manager_state();
|
||||
|
||||
let Some((surface, _)) = protocol_state
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, data)| data.instances.contains_key(resource))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let surface = surface.clone();
|
||||
|
||||
match request {
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
|
||||
state.activate(surface);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Close => {
|
||||
state.close(surface);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
|
||||
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
|
||||
state.set_fullscreen(surface, output);
|
||||
}
|
||||
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
|
||||
state.unset_fullscreen(surface);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ZwlrForeignToplevelHandleV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.instances.retain(|instance, _| instance != resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
|
||||
let mut rv = ArrayVec::new();
|
||||
if states.contains(xdg_toplevel::State::Maximized) {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
|
||||
}
|
||||
if states.contains(xdg_toplevel::State::Fullscreen) {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
|
||||
}
|
||||
|
||||
// HACK: wlr-foreign-toplevel-management states:
|
||||
//
|
||||
// These have the same meaning as the states with the same names defined in xdg-toplevel
|
||||
//
|
||||
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
|
||||
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
|
||||
// handles multiple activated windows correctly uses it in its design in such a way that
|
||||
// keyboard focus would make more sense. Let's do what the clients expect.
|
||||
if has_focus {
|
||||
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod foreign_toplevel;
|
||||
+24
-18
@@ -7,18 +7,22 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use pipewire::spa::data::DataType;
|
||||
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use pipewire::context::Context;
|
||||
use pipewire::core::Core;
|
||||
use pipewire::main_loop::MainLoop;
|
||||
use pipewire::properties::Properties;
|
||||
use pipewire::spa::buffer::DataType;
|
||||
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use pipewire::spa::param::format_utils::parse_format;
|
||||
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
||||
use pipewire::spa::param::ParamType;
|
||||
use pipewire::spa::pod::serialize::PodSerializer;
|
||||
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
||||
use pipewire::spa::sys::*;
|
||||
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
|
||||
use pipewire::spa::Direction;
|
||||
use pipewire::spa::utils::{
|
||||
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
|
||||
};
|
||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
||||
use pipewire::{Context, Core, MainLoop, Properties};
|
||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
@@ -27,22 +31,24 @@ use smithay::output::Output;
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use zbus::SignalContext;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct PipeWire {
|
||||
_context: Context<MainLoop>,
|
||||
_context: Context,
|
||||
pub core: Core,
|
||||
}
|
||||
|
||||
pub struct Cast {
|
||||
pub session_id: usize,
|
||||
pub stream: Rc<Stream>,
|
||||
pub stream: Stream,
|
||||
_listener: StreamListener<()>,
|
||||
pub is_active: Rc<Cell<bool>>,
|
||||
pub output: Output,
|
||||
pub size: Size<i32, Physical>,
|
||||
pub cursor_mode: CursorMode,
|
||||
pub last_frame_time: Duration,
|
||||
pub min_time_between_frames: Rc<Cell<Duration>>,
|
||||
@@ -51,7 +57,7 @@ pub struct Cast {
|
||||
|
||||
impl PipeWire {
|
||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||
let main_loop = MainLoop::new().context("error creating MainLoop")?;
|
||||
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
||||
let context = Context::new(&main_loop).context("error creating Context")?;
|
||||
let core = context.connect(None).context("error creating Core")?;
|
||||
|
||||
@@ -66,14 +72,14 @@ impl PipeWire {
|
||||
struct AsFdWrapper(MainLoop);
|
||||
impl AsFd for AsFdWrapper {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
self.0.fd()
|
||||
self.0.loop_().fd()
|
||||
}
|
||||
}
|
||||
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(generic, move |_, wrapper, _| {
|
||||
let _span = tracy_client::span!("pipewire iteration");
|
||||
wrapper.0.iterate(Duration::ZERO);
|
||||
wrapper.0.loop_().iterate(Duration::ZERO);
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -112,13 +118,14 @@ impl PipeWire {
|
||||
|
||||
let mode = output.current_mode().unwrap();
|
||||
let size = mode.size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
let refresh = mode.refresh;
|
||||
|
||||
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
|
||||
.context("error creating Stream")?;
|
||||
|
||||
// Like in good old wayland-rs times...
|
||||
let stream = Rc::new(stream);
|
||||
let node_id = Rc::new(Cell::new(None));
|
||||
let is_active = Rc::new(Cell::new(false));
|
||||
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
||||
@@ -127,10 +134,9 @@ impl PipeWire {
|
||||
let listener = stream
|
||||
.add_local_listener_with_user_data(())
|
||||
.state_changed({
|
||||
let stream = stream.clone();
|
||||
let is_active = is_active.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |old, new| {
|
||||
move |stream, (), old, new| {
|
||||
debug!("pw stream: state changed: {old:?} -> {new:?}");
|
||||
|
||||
match new {
|
||||
@@ -174,7 +180,7 @@ impl PipeWire {
|
||||
})
|
||||
.param_changed({
|
||||
let min_time_between_frames = min_time_between_frames.clone();
|
||||
move |stream, id, _data, pod| {
|
||||
move |stream, (), id, pod| {
|
||||
let id = ParamType::from_raw(id);
|
||||
trace!(?id, "pw stream: param_changed");
|
||||
|
||||
@@ -256,8 +262,7 @@ impl PipeWire {
|
||||
let mut b1 = vec![];
|
||||
// let mut b2 = vec![];
|
||||
let mut params = [
|
||||
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
|
||||
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
|
||||
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
|
||||
];
|
||||
stream.update_params(&mut params).unwrap();
|
||||
}
|
||||
@@ -265,7 +270,7 @@ impl PipeWire {
|
||||
.add_buffer({
|
||||
let dmabufs = dmabufs.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |buffer| {
|
||||
move |_stream, (), buffer| {
|
||||
trace!("pw stream: add_buffer");
|
||||
|
||||
unsafe {
|
||||
@@ -309,7 +314,7 @@ impl PipeWire {
|
||||
})
|
||||
.remove_buffer({
|
||||
let dmabufs = dmabufs.clone();
|
||||
move |buffer| {
|
||||
move |_stream, (), buffer| {
|
||||
trace!("pw stream: remove_buffer");
|
||||
|
||||
unsafe {
|
||||
@@ -383,6 +388,7 @@ impl PipeWire {
|
||||
_listener: listener,
|
||||
is_active,
|
||||
output,
|
||||
size,
|
||||
cursor_mode,
|
||||
last_frame_time: Duration::ZERO,
|
||||
min_time_between_frames,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
use anyhow::Context;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{Bind, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
|
||||
|
||||
pub mod nearest_integer_scale;
|
||||
pub mod offscreen;
|
||||
pub mod primary_gpu_texture;
|
||||
pub mod render_elements;
|
||||
pub mod renderer;
|
||||
|
||||
pub fn render_to_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
|
||||
let texture: GlesTexture = renderer
|
||||
.create_buffer(fourcc, buffer_size)
|
||||
.context("error creating texture")?;
|
||||
|
||||
renderer
|
||||
.bind(texture.clone())
|
||||
.context("error binding texture")?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(size, Transform::Normal)
|
||||
.context("error starting frame")?;
|
||||
|
||||
frame
|
||||
.clear([0., 0., 0., 0.], &[output_rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
|
||||
if let Some(mut damage) = output_rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
let sync_point = frame.finish().context("error finishing frame")?;
|
||||
Ok((texture, sync_point))
|
||||
}
|
||||
|
||||
pub fn render_and_download(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<GlesMapping> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let (_, sync_point) = render_to_texture(renderer, size, scale, fourcc, elements)?;
|
||||
sync_point.wait();
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
|
||||
.context("error copying framebuffer")?;
|
||||
Ok(mapping)
|
||||
}
|
||||
|
||||
pub fn render_to_vec(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
fourcc: Fourcc,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let mapping =
|
||||
render_and_download(renderer, size, scale, fourcc, elements).context("error rendering")?;
|
||||
let copy = renderer
|
||||
.map_texture(&mapping)
|
||||
.context("error mapping texture")?;
|
||||
Ok(copy.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub fn render_to_dmabuf(
|
||||
renderer: &mut GlesRenderer,
|
||||
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
|
||||
|
||||
renderer.bind(dmabuf).context("error binding texture")?;
|
||||
let mut frame = renderer
|
||||
.render(size, Transform::Normal)
|
||||
.context("error starting frame")?;
|
||||
|
||||
frame
|
||||
.clear([0., 0., 0., 0.], &[output_rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
|
||||
if let Some(mut damage) = output_rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
let _sync_point = frame.finish().context("error finishing frame")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{Frame, Renderer, TextureFilter};
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NearestIntegerScale<E: Element>(E);
|
||||
|
||||
impl<E: Element> From<E> for NearestIntegerScale<E> {
|
||||
fn from(value: E) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element> Element for NearestIntegerScale<E> {
|
||||
fn id(&self) -> &Id {
|
||||
self.0.id()
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.0.current_commit()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.0.geometry(scale)
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
self.0.transform()
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.0.src()
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.damage_since(scale, commit)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
self.0.opaque_regions(scale)
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
self.0.alpha()
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
self.0.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Renderer, E: RenderElement<R>> RenderElement<R> for NearestIntegerScale<E> {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut <R as Renderer>::Frame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), R::Error> {
|
||||
let mut use_nearest = false;
|
||||
|
||||
// Check that we don't need to interpolate between src pixels.
|
||||
let src_i32 = src.to_i32_down::<i32>();
|
||||
if src_i32.to_f64() == src {
|
||||
// Check that the src is not zero.
|
||||
if !src_i32.size.is_empty() {
|
||||
// Check that the scale factor is an integer.
|
||||
let scale_x = dst.size.w / src_i32.size.w;
|
||||
let scale_y = dst.size.h / src_i32.size.h;
|
||||
if scale_x * src_i32.size.w == dst.size.w && scale_y * src_i32.size.h == dst.size.h
|
||||
{
|
||||
use_nearest = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev_filter = TextureFilter::Linear;
|
||||
if use_nearest {
|
||||
prev_filter = frame.upscale_filter();
|
||||
frame.set_upscale_filter(TextureFilter::Nearest);
|
||||
}
|
||||
|
||||
let rv = self.0.draw(frame, src, dst, damage);
|
||||
|
||||
if use_nearest {
|
||||
frame.set_upscale_filter(prev_filter);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut R) -> Option<UnderlyingStorage> {
|
||||
self.0.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use super::render_to_texture;
|
||||
use super::renderer::AsGlesFrame;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Renders elements into an off-screen buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct OffscreenRenderElement {
|
||||
// The texture, if rendering succeeded.
|
||||
texture: Option<PrimaryGpuTextureRenderElement>,
|
||||
// The fallback buffer in case the rendering fails.
|
||||
fallback: SolidColorRenderElement,
|
||||
}
|
||||
|
||||
impl OffscreenRenderElement {
|
||||
pub fn new(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: i32,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
result_alpha: f32,
|
||||
) -> Self {
|
||||
let _span = tracy_client::span!("OffscreenRenderElement::new");
|
||||
|
||||
let geo = elements
|
||||
.iter()
|
||||
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
|
||||
.reduce(|a, b| a.merge(b))
|
||||
.unwrap_or_default();
|
||||
let logical_size = geo.size.to_logical(scale);
|
||||
|
||||
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
|
||||
let fallback = SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
geo.loc,
|
||||
Scale::from(scale as f64),
|
||||
result_alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elements = elements.iter().rev().map(|ele| {
|
||||
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||
});
|
||||
|
||||
match render_to_texture(
|
||||
renderer,
|
||||
geo.size,
|
||||
Scale::from(scale as f64),
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
) {
|
||||
Ok((texture, _sync_point)) => {
|
||||
let buffer =
|
||||
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
|
||||
let element = TextureRenderElement::from_texture_buffer(
|
||||
geo.loc.to_f64(),
|
||||
&buffer,
|
||||
Some(result_alpha),
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
Self {
|
||||
texture: Some(PrimaryGpuTextureRenderElement(element)),
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error off-screening elements: {err:?}");
|
||||
Self {
|
||||
texture: None,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for OffscreenRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.id()
|
||||
} else {
|
||||
self.fallback.id()
|
||||
}
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.current_commit()
|
||||
} else {
|
||||
self.fallback.current_commit()
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.geometry(scale)
|
||||
} else {
|
||||
self.fallback.geometry(scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.transform()
|
||||
} else {
|
||||
self.fallback.transform()
|
||||
}
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.src()
|
||||
} else {
|
||||
self.fallback.src()
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> Vec<Rectangle<i32, Physical>> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.damage_since(scale, commit)
|
||||
} else {
|
||||
self.fallback.damage_since(scale, commit)
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.opaque_regions(scale)
|
||||
} else {
|
||||
self.fallback.opaque_regions(scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.alpha()
|
||||
} else {
|
||||
self.fallback.alpha()
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.kind()
|
||||
} else {
|
||||
self.fallback.kind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
renderer: &mut TtyRenderer<'render, 'alloc>,
|
||||
) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,12 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||
};
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::renderer::AsGlesFrame;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Trait with our main renderer requirements to save on the typing.
|
||||
pub trait NiriRenderer:
|
||||
ImportAll
|
||||
+ ImportMem
|
||||
+ ExportMem
|
||||
+ Bind<Dmabuf>
|
||||
+ Offscreen<GlesTexture>
|
||||
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||
+ AsGlesRenderer
|
||||
{
|
||||
// Associated types to work around the instability of associated type bounds.
|
||||
type NiriTextureId: Texture + Clone + 'static;
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<<GlesRenderer as Renderer>::Error>
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
type NiriError = R::Error;
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesRenderer`.
|
||||
pub trait AsGlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||
}
|
||||
|
||||
impl AsGlesRenderer for GlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesFrame`.
|
||||
pub trait AsGlesFrame<'frame>
|
||||
where
|
||||
Self: 'frame,
|
||||
{
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
||||
#[derive(Debug)]
|
||||
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
||||
@@ -0,0 +1,126 @@
|
||||
// We need to implement RenderElement manually due to AsGlesFrame requirement.
|
||||
// This macro does it for us.
|
||||
#[macro_export]
|
||||
macro_rules! niri_render_elements {
|
||||
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
|
||||
#[derive(Debug)]
|
||||
pub enum $name<R: $crate::render_helpers::renderer::NiriRenderer> {
|
||||
$($variant($type)),+
|
||||
}
|
||||
|
||||
impl<R: $crate::render_helpers::renderer::NiriRenderer> smithay::backend::renderer::element::Element for $name<R> {
|
||||
fn id(&self) -> &smithay::backend::renderer::element::Id {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.id()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.current_commit()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> Rectangle<i32, smithay::utils::Physical> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.geometry(scale)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> smithay::utils::Transform {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.transform()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.src()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: smithay::utils::Scale<f64>,
|
||||
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
|
||||
) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.opaque_regions(scale)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.alpha()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> smithay::backend::renderer::element::Kind {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.kind()),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer> for $name<smithay::backend::renderer::gles::GlesRenderer> {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render, 'alloc>>
|
||||
for $name<$crate::backend::tty::TtyRenderer<'render, 'alloc>>
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, 'alloc, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
) -> Result<(), $crate::backend::tty::TtyRendererError<'render, 'alloc>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render, 'alloc>>::draw(elem, frame, src, dst, damage)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
renderer: &mut $crate::backend::tty::TtyRenderer<'render, 'alloc>,
|
||||
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
|
||||
fn from(x: $type) -> Self {
|
||||
Self::$variant(x)
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||
};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer};
|
||||
|
||||
/// Trait with our main renderer requirements to save on the typing.
|
||||
pub trait NiriRenderer:
|
||||
ImportAll
|
||||
+ ImportMem
|
||||
+ ExportMem
|
||||
+ Bind<Dmabuf>
|
||||
+ Offscreen<GlesTexture>
|
||||
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||
+ AsGlesRenderer
|
||||
{
|
||||
// Associated types to work around the instability of associated type bounds.
|
||||
type NiriTextureId: Texture + Clone + 'static;
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<<GlesRenderer as Renderer>::Error>
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
type NiriError = R::Error;
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesRenderer`.
|
||||
pub trait AsGlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||
}
|
||||
|
||||
impl AsGlesRenderer for GlesRenderer {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
|
||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesFrame`.
|
||||
pub trait AsGlesFrame<'frame>
|
||||
where
|
||||
Self: 'frame,
|
||||
{
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use smithay::output::{Output, WeakOutput};
|
||||
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
|
||||
const BORDER: i32 = 2;
|
||||
|
||||
@@ -42,6 +42,7 @@ pub enum ScreenshotUi {
|
||||
pub struct OutputData {
|
||||
size: Size<i32, Physical>,
|
||||
scale: i32,
|
||||
transform: Transform,
|
||||
texture: GlesTexture,
|
||||
texture_buffer: TextureBuffer<GlesTexture>,
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
@@ -94,6 +95,7 @@ impl ScreenshotUi {
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let scale = selection.0.current_scale().integer_scale();
|
||||
let selection = (
|
||||
selection.0,
|
||||
@@ -104,9 +106,9 @@ impl ScreenshotUi {
|
||||
let output_data = screenshots
|
||||
.into_iter()
|
||||
.map(|(output, texture)| {
|
||||
let output_transform = output.current_transform();
|
||||
let transform = output.current_transform();
|
||||
let output_mode = output.current_mode().unwrap();
|
||||
let size = output_transform.transform_size(output_mode.size);
|
||||
let size = transform.transform_size(output_mode.size);
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let texture_buffer = TextureBuffer::from_texture(
|
||||
renderer,
|
||||
@@ -129,6 +131,7 @@ impl ScreenshotUi {
|
||||
let data = OutputData {
|
||||
size,
|
||||
scale,
|
||||
transform,
|
||||
texture,
|
||||
texture_buffer,
|
||||
buffers,
|
||||
@@ -333,10 +336,10 @@ impl ScreenshotUi {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
|
||||
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
|
||||
if let Self::Open { output_data, .. } = self {
|
||||
let data = output_data.get(output)?;
|
||||
Some((data.size, data.scale))
|
||||
Some((data.size, data.scale, data.transform))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use directories::UserDirs;
|
||||
use git_version::git_version;
|
||||
use niri_config::Config;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||
@@ -20,6 +21,14 @@ pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
||||
(t.0.clone(), t.1.clone())
|
||||
}
|
||||
|
||||
pub fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
git_version!(fallback = "unknown commit"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_monotonic_time() -> Duration {
|
||||
let ts = clock_gettime(ClockId::Monotonic);
|
||||
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
|
||||
|
||||
+18
-4
@@ -27,7 +27,18 @@ impl Watcher {
|
||||
thread::Builder::new()
|
||||
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
||||
.spawn(move || {
|
||||
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
|
||||
// this "should" be as simple as mtime, but it does not quite work in practice;
|
||||
// it doesn't work if the config is a symlink, and its target changes but the
|
||||
// new target and old target have identical mtimes.
|
||||
//
|
||||
// in practice, this does not occur on any systems other than nix.
|
||||
// because, on nix practically everything is a symlink to /nix/store
|
||||
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
|
||||
// so, symlink targets change frequently when mtime doesn't.
|
||||
let mut last_props = path
|
||||
.canonicalize()
|
||||
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||
.ok();
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
@@ -36,8 +47,11 @@ impl Watcher {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
|
||||
if last_mtime != Some(mtime) {
|
||||
if let Ok(new_props) = path
|
||||
.canonicalize()
|
||||
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||
{
|
||||
if last_props.as_ref() != Some(&new_props) {
|
||||
trace!("file changed: {}", path.to_string_lossy());
|
||||
|
||||
if let Err(err) = changed.send(()) {
|
||||
@@ -45,7 +59,7 @@ impl Watcher {
|
||||
break;
|
||||
}
|
||||
|
||||
last_mtime = Some(mtime);
|
||||
last_props = Some(new_props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user