Compare commits

..

1 Commits

Author SHA1 Message Date
Ivan Molodetskikh 012340c5f4 Freeze view when pointer or touch is grabbed 2024-10-28 21:18:26 +03:00
5231 changed files with 12306 additions and 101343 deletions
-11
View File
@@ -1,12 +1 @@
# LFS configuration for images from the wiki
*.png filter=lfs diff=lfs merge=lfs -text
# Exclude LFS-tracked files from the tarball
/wiki/img/ export-ignore
# exclude .gitattributes itself from the tarball
.gitattributes export-ignore
# tip: can be tested using
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
+1 -15
View File
@@ -9,23 +9,9 @@ assignees: ''
<!-- Please describe the issue here at the top, then fill in the system information below. -->
<!-- Attaching your full niri config can help diagnose the problem. -->
<!--
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
You can also check what process the window PID belongs to:
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
If this points to xwayland-satellite, then it's an X11 window.
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
-->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
-6
View File
@@ -2,9 +2,3 @@ contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
- name: Ask a question
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
about: Chat about niri with other users
-22
View File
@@ -1,22 +0,0 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
groups:
smithay:
patterns:
- "smithay"
- "smithay-drm-extras"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
+28 -73
View File
@@ -9,8 +9,6 @@ on:
env:
RUN_SLOW_TESTS: 1
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
jobs:
build:
@@ -25,7 +23,8 @@ jobs:
release-flag: '--release'
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -34,8 +33,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -64,53 +63,19 @@ jobs:
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build tests
- name: Build Tests
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
# Job that runs randomized tests for a longer period of time.
randomized-tests:
strategy:
fail-fast: false
name: randomized tests
runs-on: ubuntu-24.04
env:
RUST_BACKTRACE: 1
PROPTEST_CASES: 200000
PROPTEST_MAX_LOCAL_REJECTS: 200000
PROPTEST_MAX_GLOBAL_REJECTS: 200000
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests --release
- name: Test
run: cargo test --all --exclude niri-visual-tests --release
visual-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -119,8 +84,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -133,8 +98,9 @@ jobs:
strategy:
fail-fast: false
name: msrv
runs-on: ubuntu-24.04
name: 'msrv - 1.77.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -143,10 +109,10 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@1.80.1
- uses: dtolnay/rust-toolchain@1.77.0
- uses: Swatinem/rust-cache@v2
@@ -157,7 +123,8 @@ jobs:
fail-fast: false
name: clippy
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -166,8 +133,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -179,7 +146,7 @@ jobs:
run: cargo clippy --all --all-targets
rustfmt:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -194,8 +161,8 @@ jobs:
run: cargo fmt --all -- --check
fedora:
runs-on: ubuntu-24.04
container: fedora:41
runs-on: ubuntu-22.04
container: fedora:39
steps:
- uses: actions/checkout@v4
@@ -205,13 +172,13 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel libdisplay-info-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
nix:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
@@ -228,36 +195,24 @@ jobs:
- run: nix flake check
continue-on-error: true
check-links:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
with:
args: --offline --include-fragments 'wiki/*.md'
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- build
- check-links
needs: build
permissions:
contents: write
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
rustdoc:
needs: build
permissions:
contents: write
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
-63
View File
@@ -1,63 +0,0 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
version:
description: 'Public version'
required: true
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
RUN_SLOW_TESTS: 1
jobs:
prepare-release:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
- name: Create vendored dependencies archive
run: |
mkdir .cargo
cargo vendor --locked > .cargo/config.toml
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
- name: Build
run: cargo build --all --frozen --release
- name: Build tests
run: cargo test --no-run --all --frozen --release
- name: Test
run: cargo test --all --frozen --release -- --nocapture
- name: Draft release
uses: softprops/action-gh-release@v2
with:
draft: true
tag_name: v${{ github.event.inputs.version }}
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
fail_on_unmatched_files: true
Generated
+1060 -1093
View File
File diff suppressed because it is too large Load Diff
+40 -55
View File
@@ -1,38 +1,32 @@
[workspace]
members = [
"niri-config",
"niri-ipc",
"niri-visual-tests",
]
members = ["niri-visual-tests"]
[workspace.package]
version = "25.5.1"
version = "0.1.9"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.80.1"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.98"
bitflags = "2.9.1"
clap = { version = "4.5.38", features = ["derive"] }
insta = "1.43.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracy-client = { version = "0.18.0", default-features = false }
anyhow = "1.0.90"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.132"
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.4", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
@@ -53,45 +47,45 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "2.4.0", optional = true }
async-io = { version = "1.13.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.23.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
bytemuck = { version = "1.19.0", features = ["derive"] }
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
clap_complete = "4.5.50"
directories = "6.0.0"
directories = "5.0.1"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
fastrand = "2.1.1"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.30.3"
glam = "0.29.0"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.172"
libdisplay-info = "0.2.2"
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.5.1", path = "niri-config" }
niri-ipc = { version = "25.5.1", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.0.0"
pango = { version = "0.20.10", features = ["v1_44"] }
pangocairo = "0.20.10"
libc = "0.2.161"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.9", path = "niri-config" }
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.4.0"
pango = { version = "0.20.4", features = ["v1_44"] }
pangocairo = "0.20.4"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.16"
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
png = "0.17.14"
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.5"
sd-notify = "0.4.3"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.10"
wayland-scanner = "0.31.6"
url = { version = "2.5.2", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
xcursor = "0.3.8"
zbus = { version = "5.7.0", optional = true }
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -113,18 +107,15 @@ features = [
[dev-dependencies]
approx = "0.5.1"
calloop-wayland-source = "0.4.0"
insta.workspace = true
proptest = "1.6.0"
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
rayon = "1.10.0"
wayland-client = "0.31.10"
xshell = "0.2.7"
k9.workspace = true
proptest = "1.5.0"
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
xshell = "0.2.6"
[features]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
dbus = ["zbus", "async-io", "notify-rust", "url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
@@ -133,8 +124,6 @@ xdp-gnome-screencast = ["dbus", "pipewire"]
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables the on-demand Tracy profiler instrumentation.
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
# Enables Tracy allocation profiling.
profile-with-tracy-allocations = ["profile-with-tracy"]
# Enables dinit integration (global environment).
dinit = []
@@ -147,12 +136,8 @@ lto = "thin"
# knuffel with chomsky generates a metric ton of debuginfo.
debug = false
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "25.02"
version = "0.1.9"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+9 -47
View File
@@ -1,7 +1,7 @@
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
@@ -10,7 +10,7 @@
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
## About
@@ -28,15 +28,12 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
@@ -48,35 +45,10 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
## Status
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Here are some points you may have questions about:
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
- **NVIDIA**: seems to work fine.
- **Floating windows**: yes, starting from niri 25.01.
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
We have touchpad gestures, but no touchscreen gestures yet.
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
- Steam and games, including Proton: work perfectly through xwayland-satellite.
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
- Discord and other Electron apps: work well through xwayland-satellite.
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
@@ -90,17 +62,10 @@ Here are some other projects which implement a similar workflow:
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [papersway]: scrollable tiling on top of sway/i3.
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Media
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
@@ -110,9 +75,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[gamescope]: https://github.com/ValveSoftware/gamescope
Generated
+9 -9
View File
@@ -2,11 +2,11 @@
"nodes": {
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1742707865,
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1742697269,
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"lastModified": 1727663505,
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
"type": "github"
},
"original": {
+14 -24
View File
@@ -26,20 +26,21 @@
{
lib,
cairo,
clang,
dbus,
libGL,
libclang,
libdisplay-info,
libinput,
seatd,
libseat,
libxkbcommon,
libgbm,
mesa,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
installShellFiles,
withDbus ? true,
withSystemd ? true,
withScreencastSupport ? true,
@@ -78,9 +79,8 @@
strictDeps = true;
nativeBuildInputs = [
rustPlatform.bindgenHook
clang
pkg-config
installShellFiles
];
buildInputs =
@@ -90,9 +90,9 @@
libGL
libdisplay-info
libinput
seatd
libseat
libxkbcommon
libgbm
mesa # libgbm
pango
wayland
]
@@ -108,22 +108,8 @@
++ lib.optional withSystemd "systemd";
buildNoDefaultFeatures = true;
# ever since this commit:
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
# and thus creates a real socket file in the runtime dir.
# this is fine for our build, we just need to make sure it has a directory to write to.
preCheck = ''
export XDG_RUNTIME_DIR="$(mktemp -d)"
'';
postInstall =
''
installShellCompletion --cmd niri \
--bash <($out/bin/niri completions bash) \
--fish <($out/bin/niri completions fish) \
--zsh <($out/bin/niri completions zsh)
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
''
@@ -133,9 +119,11 @@
'';
env = {
LIBCLANG_PATH = lib.getLib libclang + "/lib";
# Force linking with libEGL and libwayland-client
# so they can be discovered by `dlopen()`
RUSTFLAGS = toString (
CARGO_BUILD_RUSTFLAGS = toString (
map (arg: "-C link-arg=" + arg) [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
@@ -203,7 +191,7 @@
];
nativeBuildInputs = [
pkgs.rustPlatform.bindgenHook
pkgs.clang
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
@@ -213,12 +201,14 @@
];
env = {
inherit (niri) LIBCLANG_PATH;
# WARN: Do not overwrite this variable in your shell!
# It is required for `dlopen()` to work on some libraries; see the comment
# in the package expression
#
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
inherit (niri) CARGO_BUILD_RUSTFLAGS;
};
};
}
+5 -4
View File
@@ -11,13 +11,14 @@ repository.workspace = true
bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.5.1", path = "../niri-ipc" }
regex = "1.11.1"
miette = "5.10.0"
niri-ipc = { version = "0.1.9", path = "../niri-ipc" }
regex = "1.11.0"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
insta.workspace = true
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
-30
View File
@@ -1,30 +0,0 @@
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
#[knuffel(children(name = "match"))]
pub matches: Vec<Match>,
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
+342 -2231
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
use std::str::FromStr;
use regex::Regex;
/// `Regex` that implements `PartialEq` by its string form.
#[derive(Debug, Clone)]
pub struct RegexEq(pub Regex);
impl PartialEq for RegexEq {
fn eq(&self, other: &Self) -> bool {
self.0.as_str() == other.0.as_str()
}
}
impl Eq for RegexEq {}
impl FromStr for RegexEq {
type Err = <Regex as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Regex::from_str(s).map(Self)
}
}
+2 -6
View File
@@ -1,19 +1,15 @@
[package]
name = "niri-ipc"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
description = "Types and helpers for interfacing with the niri Wayland compositor."
keywords = ["wayland"]
categories = ["api-bindings", "os"]
readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "0.8.22", optional = true }
schemars = { version = "0.8.21", optional = true }
serde.workspace = true
serde_json.workspace = true
-16
View File
@@ -1,16 +0,0 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
## Backwards compatibility
This crate follows the niri version.
It is **not** API-stable in terms of the Rust semver.
In particular, expect new struct fields and enum variants to be added in patch version bumps.
Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.5.1"
```
+11 -569
View File
@@ -1,23 +1,8 @@
//! Types for communicating with niri via IPC.
//!
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
//! wrapping a [`Response`].
//!
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
//! event stream and write more requests at the same time, you need to use two IPC sockets.
//!
//! <div class="warning">
//!
//! Requests are *always* processed separately. Time passes between requests, even when sending
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
//! new workspace in-between the two responses). This goes for actions too: sending
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
//! the wrong window because a different window got focused in-between these requests.
//!
//! </div>
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
//! can keep reading [`Event`]s from the socket after the response.
//!
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
@@ -27,29 +12,13 @@
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
//! separate line.
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! on a single line each.
//!
//! ## Backwards compatibility
//!
//! This crate follows the niri version. It is **not** API-stable in terms of the Rust semver. In
//! particular, expect new struct fields and enum variants to be added in patch version bumps.
//!
//! Use an exact version requirement to avoid breaking changes:
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.5.1"
//! ```
//!
//! ## Features
//!
//! This crate defines the following features:
//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
//! the types.
//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
#![warn(missing_docs)]
use std::collections::HashMap;
@@ -72,18 +41,12 @@ pub enum Request {
Workspaces,
/// Request information about open windows.
Windows,
/// Request information about layer-shell surfaces.
Layers,
/// Request information about the configured keyboard layouts.
KeyboardLayouts,
/// Request information about the focused output.
FocusedOutput,
/// Request information about the focused window.
FocusedWindow,
/// Request picking a window and get its information.
PickWindow,
/// Request picking a color from the screen.
PickColor,
/// Perform an action.
Action(Action),
/// Change output configuration temporarily.
@@ -114,8 +77,6 @@ pub enum Request {
EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
/// Request information about the overview.
OverviewState,
}
/// Reply from niri to client.
@@ -144,38 +105,14 @@ pub enum Response {
Workspaces(Vec<Workspace>),
/// Information about open windows.
Windows(Vec<Window>),
/// Information about layer-shell surfaces.
Layers(Vec<LayerSurface>),
/// Information about the keyboard layout.
KeyboardLayouts(KeyboardLayouts),
/// Information about the focused output.
FocusedOutput(Option<Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
/// Information about the picked window.
PickedWindow(Option<Window>),
/// Information about the picked color.
PickedColor(Option<PickedColor>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about the overview.
OverviewState(Overview),
}
/// Overview information.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Overview {
/// Whether the overview is currently open.
pub is_open: bool,
}
/// Color picked from the screen.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PickedColor {
/// Color values as red, green, blue, each ranging from 0.0 to 1.0.
pub rgb: [f64; 3],
}
/// Actions that niri can perform.
@@ -210,23 +147,9 @@ pub enum Action {
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot {
/// Whether to show the mouse pointer by default in the screenshot UI.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
Screenshot {},
/// Screenshot the focused screen.
ScreenshotScreen {
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
/// Whether to include the mouse pointer in the screenshot.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
ScreenshotScreen {},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
ScreenshotWindow {
@@ -235,14 +158,7 @@ pub enum Action {
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
},
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
ToggleKeyboardShortcutsInhibit {},
/// Close a window.
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
CloseWindow {
@@ -264,34 +180,12 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle windowed (fake) fullscreen on a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
)]
ToggleWindowedFullscreen {
/// Id of the window to toggle windowed fullscreen of.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus a window by id.
FocusWindow {
/// Id of the window to focus.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus a window in the focused column by index.
FocusWindowInColumn {
/// Index of the window in the column.
///
/// The index starts from 1 for the topmost window.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Focus the previously focused window.
FocusWindowPrevious {},
/// Focus the column to the left.
FocusColumnLeft {},
/// Focus the column to the right.
@@ -304,14 +198,6 @@ pub enum Action {
FocusColumnRightOrFirst {},
/// Focus the next column to the left, looping if at start.
FocusColumnLeftOrLast {},
/// Focus a column by index.
FocusColumn {
/// Index of the column to focus.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Focus the window or the monitor above.
FocusWindowOrMonitorUp {},
/// Focus the window or the monitor below.
@@ -336,14 +222,6 @@ pub enum Action {
FocusWindowOrWorkspaceDown {},
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp {},
/// Focus the topmost window.
FocusWindowTop {},
/// Focus the bottommost window.
FocusWindowBottom {},
/// Focus the window below or the topmost window.
FocusWindowDownOrTop {},
/// Focus the window above or the bottommost window.
FocusWindowUpOrBottom {},
/// Move the focused column to the left.
MoveColumnLeft {},
/// Move the focused column to the right.
@@ -356,14 +234,6 @@ pub enum Action {
MoveColumnLeftOrToMonitorLeft {},
/// Move the focused column to the right or to the monitor to the right.
MoveColumnRightOrToMonitorRight {},
/// Move the focused column to a specific index on its workspace.
MoveColumnToIndex {
/// New index for the column.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Move the focused window down in a column.
MoveWindowDown {},
/// Move the focused window up in a column.
@@ -400,34 +270,8 @@ pub enum Action {
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right.
SwapWindowRight {},
/// Swap focused window with one to the left.
SwapWindowLeft {},
/// Toggle the focused column between normal and tabbed display.
ToggleColumnTabbedDisplay {},
/// Set the display mode of the focused column.
SetColumnDisplay {
/// Display mode to set.
#[cfg_attr(feature = "clap", arg())]
display: ColumnDisplay,
},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
#[cfg_attr(
feature = "clap",
clap(about = "Center the focused window on the screen")
)]
CenterWindow {
/// Id of the window to center.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Center all fully visible columns on the screen.
CenterVisibleColumns {},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -459,94 +303,21 @@ pub enum Action {
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the moved window.
///
/// If `true` (the default) and the window to move is focused, the focus will follow the
/// window to the new workspace. If `false`, the focus will remain on the original
/// workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveColumnToWorkspaceDown {},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveColumnToWorkspaceUp {},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused workspace down.
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Move a workspace to a specific index on its monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific index on its monitor")
)]
MoveWorkspaceToIndex {
/// New index for the workspace.
#[cfg_attr(feature = "clap", arg())]
index: usize,
/// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Set the name of the focused workspace")
)]
SetWorkspaceName {
/// New name for the workspace.
#[cfg_attr(feature = "clap", arg())]
name: String,
/// Reference (index or name) of the workspace to name.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
workspace: Option<WorkspaceReferenceArg>,
},
/// Unset the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Unset the name of the focused workspace")
)]
UnsetWorkspaceName {
/// Reference (index or name) of the workspace to unname.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg())]
reference: Option<WorkspaceReferenceArg>,
},
/// Focus the monitor to the left.
FocusMonitorLeft {},
/// Focus the monitor to the right.
@@ -555,16 +326,6 @@ pub enum Action {
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp {},
/// Focus the previous monitor.
FocusMonitorPrevious {},
/// Focus the next monitor.
FocusMonitorNext {},
/// Focus a monitor by name.
FocusMonitor {
/// Name of the output to focus.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
@@ -573,26 +334,6 @@ pub enum Action {
MoveWindowToMonitorDown {},
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp {},
/// Move the focused window to the previous monitor.
MoveWindowToMonitorPrevious {},
/// Move the focused window to the next monitor.
MoveWindowToMonitorNext {},
/// Move a window to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused window to a specific monitor")
)]
MoveWindowToMonitor {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
@@ -601,32 +342,6 @@ pub enum Action {
MoveColumnToMonitorDown {},
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp {},
/// Move the focused column to the previous monitor.
MoveColumnToMonitorPrevious {},
/// Move the focused column to the next monitor.
MoveColumnToMonitorNext {},
/// Move the focused column to a specific monitor.
MoveColumnToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Change the width of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Change the width of the focused window")
)]
SetWindowWidth {
/// Id of the window whose width to set.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the width.
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Change the height of a window.
#[cfg_attr(
feature = "clap",
@@ -640,7 +355,7 @@ pub enum Action {
id: Option<u64>,
/// How to change the height.
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Reset the height of a window back to automatic.
@@ -657,14 +372,6 @@ pub enum Action {
},
/// Switch between preset column widths.
SwitchPresetColumnWidth {},
/// Switch between preset window widths.
SwitchPresetWindowWidth {
/// Id of the window whose width to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
@@ -678,11 +385,9 @@ pub enum Action {
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Expand the focused column to space not taken up by other fully visible columns.
ExpandColumnToAvailableWidth {},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
@@ -699,147 +404,12 @@ pub enum Action {
MoveWorkspaceToMonitorDown {},
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp {},
/// Move the focused workspace to the previous monitor.
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
/// Move a workspace to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific monitor")
)]
MoveWorkspaceToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage {},
/// Move the focused window between the floating and the tiling layout.
ToggleWindowFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the floating layout.
MoveWindowToFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the tiling layout.
MoveWindowToTiling {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switches focus to the floating layout.
FocusFloating {},
/// Switches focus to the tiling layout.
FocusTiling {},
/// Toggles the focus between the floating and the tiling layout.
SwitchFocusBetweenFloatingAndTiling {},
/// Move a floating window on screen.
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
MoveFloatingWindow {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the X position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
x: PositionChange,
/// How to change the Y position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
y: PositionChange,
},
/// Toggle the opacity of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle the opacity of the focused window")
)]
ToggleWindowRuleOpacity {
/// Id of the window.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a window.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused window")
)]
SetDynamicCastWindow {
/// Id of the window to target.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused monitor")
)]
SetDynamicCastMonitor {
/// Name of the output to target.
///
/// If `None`, uses the focused output.
#[cfg_attr(feature = "clap", arg())]
output: Option<String>,
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle (open/close) the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
/// Toggle urgent status of a window.
ToggleWindowUrgent {
/// Id of the window to toggle urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Set urgent status of a window.
SetWindowUrgent {
/// Id of the window to set urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Unset urgent status of a window.
UnsetWindowUrgent {
/// Id of the window to unset urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
}
/// Change in window or column size.
@@ -856,16 +426,6 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Change in floating window position.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum PositionChange {
/// Set the position in logical pixels.
SetFixed(f64),
/// Add or subtract to the current position in logical pixels.
AdjustFixed(f64),
}
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -886,18 +446,6 @@ pub enum LayoutSwitchTarget {
Next,
/// The previous configured layout.
Prev,
/// The specific layout by index.
Index(u8),
}
/// How windows display in a column.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ColumnDisplay {
/// Windows are tiled vertically across the working area height.
Normal,
/// Windows are in tabs.
Tabbed,
}
/// Output actions that niri can perform.
@@ -1133,23 +681,12 @@ pub struct Window {
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
/// Process ID that created the Wayland connection for this window, if known.
///
/// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
/// change in the future.
pub pid: Option<i32>,
/// Id of the workspace this window is on, if any.
pub workspace_id: Option<u64>,
/// Whether this window is currently focused.
///
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
pub is_focused: bool,
/// Whether this window is currently floating.
///
/// If the window isn't floating then it is in the tiling layout.
pub is_floating: bool,
/// Whether this window requests your attention.
pub is_urgent: bool,
}
/// Output configuration change result.
@@ -1189,8 +726,6 @@ pub struct Workspace {
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace currently has an urgent window in its output.
pub is_urgent: bool,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
@@ -1213,46 +748,6 @@ pub struct KeyboardLayouts {
pub current_idx: u8,
}
/// A layer-shell layer.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum Layer {
/// The background layer.
Background,
/// The bottom layer.
Bottom,
/// The top layer.
Top,
/// The overlay layer.
Overlay,
}
/// Keyboard interactivity modes for a layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum LayerSurfaceKeyboardInteractivity {
/// Surface cannot receive keyboard focus.
None,
/// Surface receives keyboard focus whenever possible.
Exclusive,
/// Surface receives keyboard focus on demand, e.g. when clicked.
OnDemand,
}
/// A layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct LayerSurface {
/// Namespace provided by the layer-shell client.
pub namespace: String,
/// Name of the output the surface is on.
pub output: String,
/// Layer that the surface is on.
pub layer: Layer,
/// The surface's keyboard interactivity mode.
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -1265,13 +760,6 @@ pub enum Event {
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// The workspace urgency changed.
WorkspaceUrgencyChanged {
/// Id of the workspace.
id: u64,
/// Whether this workspace has an urgent window.
urgent: bool,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
@@ -1319,13 +807,6 @@ pub enum Event {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// Window urgency changed.
WindowUrgencyChanged {
/// Id of the window.
id: u64,
/// The new urgency state of the window.
urgent: bool,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
@@ -1336,11 +817,6 @@ pub enum Event {
/// Index of the newly active layout.
idx: u8,
},
/// The overview was opened or closed.
OverviewOpenedOrClosed {
/// The new state of the overview.
is_open: bool,
},
}
impl FromStr for WorkspaceReferenceArg {
@@ -1401,25 +877,6 @@ impl FromStr for SizeChange {
}
}
impl FromStr for PositionChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
@@ -1427,22 +884,7 @@ impl FromStr for LayoutSwitchTarget {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
other => match other.parse() {
Ok(layout) => Ok(Self::Index(layout)),
_ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
},
}
}
}
impl FromStr for ColumnDisplay {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"tabbed" => Ok(Self::Tabbed),
_ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
}
}
}
+18 -42
View File
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: BufReader<UnixStream>,
stream: UnixStream,
}
impl Socket {
@@ -37,7 +37,6 @@ impl Socket {
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
let stream = BufReader::new(stream);
Ok(Self { stream })
}
@@ -48,54 +47,31 @@ impl Socket {
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_string(&request).unwrap();
buf.push('\n');
self.stream.get_mut().write_all(buf.as_bytes())?;
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
buf.clear();
self.stream.read_line(&mut buf)?;
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
Ok(reply)
}
/// Starts reading event stream [`Event`]s from the socket.
///
/// The returned function will block until the next [`Event`] arrives, then return it.
///
/// Use this only after requesting an [`EventStream`][Request::EventStream].
///
/// # Examples
///
/// ```no_run
/// use niri_ipc::{Request, Response};
/// use niri_ipc::socket::Socket;
///
/// fn main() -> std::io::Result<()> {
/// let mut socket = Socket::connect()?;
///
/// let reply = socket.send(Request::EventStream)?;
/// if matches!(reply, Ok(Response::Handled)) {
/// let mut read_event = socket.read_events();
/// while let Ok(event) = read_event() {
/// println!("Received event: {event:?}");
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
let Self { mut stream } = self;
let _ = stream.get_mut().shutdown(Shutdown::Write);
let mut buf = String::new();
move || {
let events = move || {
buf.clear();
stream.read_line(&mut buf)?;
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
}
};
Ok((reply, events))
}
}
-45
View File
@@ -40,9 +40,6 @@ pub struct EventStreamState {
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
/// State of the overview.
pub overview: OverviewState,
}
/// The workspaces state communicated over the event stream.
@@ -66,20 +63,12 @@ pub struct KeyboardLayoutsState {
pub keyboard_layouts: Option<KeyboardLayouts>,
}
/// The overview state communicated over the event stream.
#[derive(Debug, Default)]
pub struct OverviewState {
/// Whether the overview is currently open.
pub is_open: bool,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events.extend(self.overview.replicate());
events
}
@@ -87,7 +76,6 @@ impl EventStreamStatePart for EventStreamState {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
let event = self.overview.apply(event)?;
Some(event)
}
}
@@ -103,13 +91,6 @@ impl EventStreamStatePart for WorkspacesState {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
for ws in self.workspaces.values_mut() {
if ws.id == id {
ws.is_urgent = urgent;
}
}
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
@@ -181,14 +162,6 @@ impl EventStreamStatePart for WindowsState {
win.is_focused = Some(win.id) == id;
}
}
Event::WindowUrgencyChanged { id, urgent } => {
for win in self.windows.values_mut() {
if win.id == id {
win.is_urgent = urgent;
break;
}
}
}
event => return Some(event),
}
None
@@ -219,21 +192,3 @@ impl EventStreamStatePart for KeyboardLayoutsState {
None
}
}
impl EventStreamStatePart for OverviewState {
fn replicate(&self) -> Vec<Event> {
vec![Event::OverviewOpenedOrClosed {
is_open: self.is_open,
}]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::OverviewOpenedOrClosed { is_open } => {
self.is_open = is_open;
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.5.1", path = ".." }
niri-config = { version = "25.5.1", path = "../niri-config" }
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.9", path = ".." }
niri-config = { version = "0.1.9", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+16 -8
View File
@@ -1,13 +1,15 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientAngle {
angle: f32,
@@ -15,7 +17,7 @@ pub struct GradientAngle {
}
impl GradientAngle {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
@@ -29,13 +31,20 @@ impl TestCase for GradientAngle {
}
fn advance_animations(&mut self, current_time: Duration) {
let delta = if self.prev_time.is_zero() {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.angle += delta.as_secs_f32() * PI;
if self.angle >= PI * 2. {
@@ -50,20 +59,19 @@ impl TestCase for GradientAngle {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+17 -13
View File
@@ -1,14 +1,16 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientArea {
progress: f32,
@@ -17,16 +19,14 @@ pub struct GradientArea {
}
impl GradientArea {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
urgent_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
});
Self {
@@ -43,13 +43,20 @@ impl TestCase for GradientArea {
}
fn advance_animations(&mut self, current_time: Duration) {
let delta = if self.prev_time.is_zero() {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.progress += delta.as_secs_f32() * PI;
if self.progress >= PI * 2. {
@@ -67,8 +74,8 @@ impl TestCase for GradientArea {
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
@@ -76,18 +83,16 @@ impl TestCase for GradientArea {
));
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::new(g_loc, g_size);
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update_render_elements(
g_size,
true,
true,
false,
Rectangle::default(),
CornerRadius::default(),
1.,
1.,
);
rv.extend(
self.border
@@ -103,11 +108,10 @@ impl TestCase for GradientArea {
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_size(rect_size).to_f64(),
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklab {
gradient_format: GradientInterpolation,
}
impl GradientOklab {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklab {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklabAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklabAlpha {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -29,20 +29,19 @@ impl TestCase for GradientOklabAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklchAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklchAlpha {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklchAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklchDecreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchDecreasing {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklchDecreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklchIncreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchIncreasing {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklchIncreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklchLonger {
gradient_format: GradientInterpolation,
}
impl GradientOklchLonger {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklchLonger {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientOklchShorter {
gradient_format: GradientInterpolation,
}
impl GradientOklchShorter {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,20 +31,19 @@ impl TestCase for GradientOklchShorter {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+6 -7
View File
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientSrgb {
gradient_format: GradientInterpolation,
}
impl GradientSrgb {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -31,20 +31,19 @@ impl TestCase for GradientSrgb {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientSrgbAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbAlpha {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -29,20 +29,19 @@ impl TestCase for GradientSrgbAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientSrgbLinear {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinear {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -31,20 +31,19 @@ impl TestCase for GradientSrgbLinear {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::{Args, TestCase};
use super::TestCase;
pub struct GradientSrgbLinearAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinearAlpha {
pub fn new(_args: Args) -> Self {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -29,20 +29,19 @@ impl TestCase for GradientSrgbLinearAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_size(area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+46 -86
View File
@@ -1,17 +1,18 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::animation::Clock;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
use niri::layout::workspace::ColumnWidth;
use niri::layout::{LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
use niri::utils::get_monotonic_time;
use niri_config::{Color, FloatOrInt, OutputName};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::utils::{Physical, Size};
use smithay::utils::{Logical, Physical, Size};
use super::{Args, TestCase};
use super::TestCase;
use crate::test_window::TestWindow;
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
@@ -19,16 +20,13 @@ type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
pub struct Layout {
output: Output,
windows: Vec<TestWindow>,
clock: Clock,
layout: niri::layout::Layout<TestWindow>,
start_time: Duration,
steps: HashMap<Duration, DynStepFn>,
}
impl Layout {
pub fn new(args: Args) -> Self {
let Args { size, clock } = args;
pub fn new(size: Size<i32, Logical>) -> Self {
let output = Output::new(
String::new(),
PhysicalProperties {
@@ -60,51 +58,46 @@ impl Layout {
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
let mut layout = niri::layout::Layout::with_options(options);
layout.add_output(output.clone());
let start_time = clock.now_unadjusted();
Self {
output,
windows: Vec::new(),
clock,
layout,
start_time,
start_time: get_monotonic_time(),
steps: HashMap::new(),
}
}
pub fn open_in_between(args: Args) -> Self {
let mut rv = Self::new(args);
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
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(&0);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
rv
}
pub fn open_multiple_quickly(args: Args) -> Self {
let mut rv = Self::new(args);
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(PresetSize::Proportion(0.3)));
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -112,13 +105,13 @@ impl Layout {
rv
}
pub fn open_multiple_quickly_big(args: Args) -> Self {
let mut rv = Self::new(args);
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(PresetSize::Proportion(0.5)));
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -126,59 +119,44 @@ impl Layout {
rv
}
pub fn open_to_the_left(args: Args) -> Self {
let mut rv = Self::new(args);
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
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(PresetSize::Proportion(0.3)));
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
rv
}
pub fn open_to_the_left_big(args: Args) -> Self {
let mut rv = Self::new(args);
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(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
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(PresetSize::Proportion(0.5)));
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
rv
}
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout.add_window(
window.clone(),
AddWindowTarget::Auto,
width,
None,
false,
false,
ActivateWindow::default(),
);
self.layout.add_window(window.clone(), width, false);
self.windows.push(window);
}
@@ -186,28 +164,14 @@ impl Layout {
&mut self,
right_of: &TestWindow,
mut window: TestWindow,
width: Option<PresetSize>,
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout.add_window(
window.clone(),
AddWindowTarget::NextTo(right_of.id()),
width,
None,
false,
false,
ActivateWindow::default(),
);
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
self.windows.push(window);
}
@@ -237,25 +201,22 @@ impl TestCase for Layout {
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, _current_time: Duration) {
let now_unadjusted = self.clock.now_unadjusted();
fn advance_animations(&mut self, mut current_time: Duration) {
let run = self
.steps
.keys()
.copied()
.filter(|delay| self.start_time + *delay <= now_unadjusted)
.filter(|delay| self.start_time + *delay <= current_time)
.collect::<Vec<_>>();
for delay in &run {
let now = self.start_time + *delay;
self.clock.set_unadjusted(now);
self.layout.advance_animations();
let f = self.steps.remove(delay).unwrap();
for key in &run {
let f = self.steps.remove(key).unwrap();
f(self);
}
if !run.is_empty() {
current_time = get_monotonic_time();
}
self.clock.set_unadjusted(now_unadjusted);
self.layout.advance_animations();
self.layout.advance_animations(current_time);
}
fn render(
@@ -267,8 +228,7 @@ impl TestCase for Layout {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.render_elements(renderer, RenderTarget::Output)
.map(|elem| Box::new(elem) as _)
.collect()
}
+1 -7
View File
@@ -1,9 +1,8 @@
use std::time::Duration;
use niri::animation::Clock;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Size};
use smithay::utils::{Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
@@ -22,11 +21,6 @@ pub mod layout;
pub mod tile;
pub mod window;
pub struct Args {
pub size: Size<i32, Logical>,
pub clock: Clock,
}
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
+37 -37
View File
@@ -6,9 +6,9 @@ use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use super::{Args, TestCase};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Tile {
@@ -17,46 +17,53 @@ pub struct Tile {
}
impl Tile {
pub fn freeform(args: Args) -> Self {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
Self::with_window(args, window)
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
pub fn fixed_size(args: Args) -> Self {
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
Self::with_window(args, window)
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
Self::with_window(args, window)
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
pub fn freeform_open(args: Args) -> Self {
let mut rv = Self::freeform(args);
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(args: Args) -> Self {
let mut rv = Self::fixed_size(args);
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(args: Args) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(args);
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(args: Args, window: TestWindow) -> Self {
let Args { size, clock } = args;
pub fn with_window(window: TestWindow) -> Self {
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
@@ -70,28 +77,15 @@ impl Tile {
},
..Default::default()
};
let mut tile = niri::layout::tile::Tile::new(
window.clone(),
size.to_f64(),
1.,
clock,
Rc::new(options),
);
tile.request_tile_size(size.to_f64(), false, None);
window.communicate();
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
Self { window, tile }
}
}
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
let size = Size::from((width, height)).to_f64();
self.tile
.update_config(size, 1., self.tile.options().clone());
self.tile.request_tile_size(size, false, None);
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
self.window.communicate();
}
@@ -99,8 +93,8 @@ impl TestCase for Tile {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, _current_time: Duration) {
self.tile.advance_animations();
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time);
}
fn render(
@@ -112,12 +106,18 @@ impl TestCase for Tile {
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update_render_elements(
self.tile.update(
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
);
self.tile
.render(renderer, location, true, RenderTarget::Output)
.render(
renderer,
location,
Scale::from(1.),
true,
RenderTarget::Output,
)
.map(|elem| Box::new(elem) as _)
.collect()
}
+9 -9
View File
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Scale, Size};
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use super::{Args, TestCase};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Window {
@@ -12,24 +12,24 @@ pub struct Window {
}
impl Window {
pub fn freeform(args: Args) -> Self {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(args.size, false, false, None);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(args: Args) -> Self {
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(args.size, false, false, None);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(args.size, false, false, None);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
@@ -38,7 +38,7 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window
.request_size(Size::from((width, height)), false, false, None);
.request_size(Size::from((width, height)), false, None);
self.window.communicate();
}
+15 -7
View File
@@ -2,11 +2,15 @@
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::Args;
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
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;
@@ -62,23 +66,24 @@ fn on_startup(_app: &adw::Application) {
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
struct S {
stack: gtk::Stack,
anim_adjustment: gtk::Adjustment,
}
impl S {
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
let view = SmithayView::new(make, &self.anim_adjustment);
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(),
anim_adjustment: anim_adjustment.clone(),
};
s.add(Window::freeform, "Freeform Window");
@@ -132,6 +137,9 @@ fn build_ui(app: &adw::Application) {
let content_headerbar = adw::HeaderBar::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
anim_adjustment
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
anim_scale.set_hexpand(true);
+49 -94
View File
@@ -1,44 +1,35 @@
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use smithay::utils::Size;
use smithay::utils::{Logical, Size};
use crate::cases::{Args, TestCase};
use crate::cases::TestCase;
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use std::time::Duration;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::animation::Clock;
use niri::render_helpers::{resources, shaders};
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
struct RendererData {
renderer: GlesRenderer,
dummy_texture: GlesTexture,
}
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<RendererData, ()>>>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
pub clock: RefCell<Clock>,
}
#[glib::object_subclass]
@@ -131,71 +122,30 @@ mod imp {
let Ok(renderer) = renderer else {
return Ok(());
};
let RendererData {
renderer,
dummy_texture,
} = renderer;
let size = self.size.get();
let frame_clock = self.obj().frame_clock().unwrap();
let time = Duration::from_micros(frame_clock.frame_time() as u64);
self.clock.borrow_mut().set_unadjusted(time);
// Create the test case if missing.
let mut case = self.test_case.borrow_mut();
let case = case.get_or_insert_with(|| {
let make = self.make_test_case.get().unwrap();
let args = Args {
size: Size::from(size),
clock: self.clock.borrow().clone(),
};
make(args)
make(Size::from(size))
});
case.advance_animations(self.clock.borrow_mut().now());
case.advance_animations(get_monotonic_time());
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
// Fetch GtkGLArea's framebuffer binding.
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
case.render(renderer, Size::from(size))
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
// This call will already change the framebuffer binding (offscreen elements will bind
// intermediate textures during rendering).
let elements = case.render(renderer, Size::from(size));
// HACK: there's currently no way to "just" render into an externally bound framebuffer
// (like we have in this case). The render() call requires a valid target. So what
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
// before rendering.
let mut dummy_target = renderer
.bind(dummy_texture)
.context("error binding dummy texture")?;
}?;
let mut frame = renderer
.render(&mut dummy_target, rect.size, Transform::Normal)
.render(rect.size, Transform::Normal)
.context("error creating frame")?;
// Now that render() bound the dummy texture, change the binding underneath it back to
// GtkGLArea's framebuffer, to render there instead.
frame
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
frame
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
.context("error clearing")?;
@@ -216,7 +166,7 @@ mod imp {
}
}
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
@@ -239,17 +189,40 @@ mod imp {
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
let dummy_texture = renderer
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
.context("error creating dummy texture")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
Ok(RendererData {
renderer,
dummy_texture,
})
Ok(renderer)
}
unsafe fn with_framebuffer_save_restore<T>(
renderer: &mut GlesRenderer,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> anyhow::Result<T> {
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
let rv = f(renderer);
renderer.unbind().context("error unbinding")?;
renderer
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
Ok(rv)
}
}
@@ -260,32 +233,14 @@ glib::wrapper! {
impl SmithayView {
pub fn new<T: TestCase + 'static>(
make_test_case: impl Fn(Args) -> T + 'static,
anim_adjustment: &gtk::Adjustment,
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
) -> Self {
let obj: Self = glib::Object::builder().build();
let make = move |args| Box::new(make_test_case(args)) as Box<dyn TestCase>;
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);
anim_adjustment.connect_value_changed({
let obj = obj.downgrade();
move |adj| {
if let Some(obj) = obj.upgrade() {
let mut clock = obj.imp().clock.borrow_mut();
let instantly = adj.value() == 0.0;
let rate = if instantly {
1.0
} else {
1.0 / adj.value().max(0.001)
};
clock.set_rate(rate);
clock.set_complete_instantly(instantly);
}
}
});
obj
}
}
+8 -20
View File
@@ -6,13 +6,12 @@ use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::offscreen::OffscreenData;
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
@@ -182,12 +181,15 @@ impl LayoutElement for TestWindow {
fn request_size(
&mut self,
size: Size<i32, Logical>,
is_fullscreen: bool,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
@@ -212,20 +214,14 @@ impl LayoutElement for TestWindow {
fn output_leave(&self, _output: &Output) {}
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&mut self, _active: bool) {}
fn set_active_in_column(&mut self, _active: bool) {}
fn set_floating(&mut self, _floating: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn is_ignoring_opacity_window_rule(&self) -> bool {
false
}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
@@ -244,10 +240,6 @@ impl LayoutElement for TestWindow {
self.inner.borrow().requested_size
}
fn is_child_of(&self, _parent: &Self) -> bool {
false
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
@@ -267,13 +259,9 @@ impl LayoutElement for TestWindow {
fn cancel_interactive_resize(&mut self) {}
fn on_commit(&mut self, _serial: Serial) {}
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
fn is_urgent(&self) -> bool {
false
}
}
+7 -9
View File
@@ -33,6 +33,7 @@ Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 OR BSL-1.0
@@ -40,21 +41,18 @@ SourceLicense: GPL-3.0-or-later
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
# BSD-2-Clause
# BSD-2-Clause OR Apache-2.0 OR MIT
# BSD-3-Clause
# BSD-3-Clause OR MIT OR Apache-2.0
# GPL-3.0-or-later
# ISC
# MIT
# MIT AND (MIT OR Apache-2.0)
# MIT OR Apache-2.0
# (MIT OR Apache-2.0) AND BSD-3-Clause
# (MIT OR Apache-2.0) AND Unicode-3.0
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unicode-3.0
# Unlicense OR MIT
# Zlib OR Apache-2.0 OR MIT
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
@@ -89,7 +87,6 @@ Recommends: gnome-keyring
Recommends: alacritty
Recommends: fuzzel
Recommends: swaylock
Recommends: waybar
# Suggested utilities
Recommends: swaybg
Recommends: mako
@@ -104,6 +101,10 @@ Opening a new window never causes existing windows to resize.
%prep
{{{ git_dir_setup_macro }}}
# Make the version log message look nicer: since we're building not from niri's git repository,
# the git version macro will show its fallback string.
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
%cargo_prep -N
# We're doing an online build.
@@ -112,9 +113,6 @@ sed -i 's/^offline = true$//' .cargo/config.toml
# Final step in leaving alone our debug settings.
sed -i 's/.*please-remove-me$//' .cargo/config.toml
# Set the commit string.
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
%build
%cargo_build
+11 -102
View File
@@ -1,7 +1,7 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// Input device configuration.
// Find the full list of options on the wiki:
@@ -16,9 +16,6 @@ input {
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
@@ -28,8 +25,6 @@ input {
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
@@ -192,50 +187,10 @@ layout {
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can enable drop shadows for windows.
shadow {
// Uncomment the next line to enable shadows.
// on
// By default, the shadow draws only around its window, and not behind it.
// Uncomment this setting to make the shadow draw behind its window.
//
// Note that niri has no way of knowing about the CSD window corner
// radius. It has to assume that windows have square corners, leading to
// shadow artifacts inside the CSD rounded corners. This setting fixes
// those artifacts.
//
// However, instead you may want to set prefer-no-csd and/or
// geometry-corner-radius. Then, niri will know the corner radius and
// draw the shadow correctly, without having to draw it behind the
// window. These will also remove client-side shadows if the window
// draws any.
//
// draw-behind-window true
// You can change how shadows look. The values below are in logical
// pixels and match the CSS box-shadow properties.
// Softness controls the shadow blur radius.
softness 30
// Spread expands the shadow.
spread 5
// Offset moves the shadow relative to the window.
offset x=0 y=5
// You can also change the shadow color and opacity.
color "#0007"
}
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
@@ -253,9 +208,7 @@ layout {
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "waybar"
// spawn-at-startup "alacritty" "-e" "fish"
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
@@ -297,15 +250,6 @@ window-rule {
default-column-width {}
}
// Open the Firefox picture-in-picture player as floating by default.
window-rule {
// This app-id regular expression will work for both:
// - host Firefox (app-id is "firefox")
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
open-floating true
}
// Example: block out two password managers from screen capture.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
@@ -341,9 +285,9 @@ binds {
Mod+Shift+Slash { show-hotkey-overlay; }
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Super+Alt+L { spawn "swaylock"; }
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
@@ -356,11 +300,6 @@ binds {
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
@@ -502,32 +441,22 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// The following binds move the focused window in and out of a column.
// If the window is alone, they will consume it into the nearby column to the side.
// If the window is already in a column, they will expel it out.
// Consume one window from the right into the focused column.
Mod+Comma { consume-window-into-column; }
// Expel one window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// There are also commands that consume or expel a single window to the side.
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
// Consume one window from the right to the bottom of the focused column.
Mod+Comma { consume-window-into-column; }
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// Expand the focused column to space not taken up by other fully visible columns.
// Makes the column "fill the rest of the space".
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
// Center all fully visible columns on screen.
Mod+Ctrl+C { center-visible-columns; }
// Finer width adjustments.
// This command can also:
// * set width in pixels: "1000"
@@ -543,15 +472,6 @@ binds {
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// Move the focused window between the floating and the tiling layout.
Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
// Toggle tabbed column display mode.
// Windows in this column will appear as vertical tabs,
// rather than stacked on top of each other.
Mod+W { toggle-column-tabbed-display; }
// Actions to switch layouts.
// Note: if you uncomment these, make sure you do NOT have
// a matching layout switch hotkey configured in xkb options above.
@@ -564,19 +484,8 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// Applications such as remote-desktop clients and software KVM switches may
// request that niri stops processing the keyboard shortcuts defined here
// so they may, for example, forward the key presses as-is to a remote machine.
// It's a good idea to bind an escape hatch to toggle the inhibitor,
// so a buggy application can't hold your session hostage.
//
// The allow-inhibiting=false property can be applied to other binds as well,
// which ensures niri always processes them, even when an inhibitor is active.
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
-2
View File
@@ -1,5 +1,3 @@
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk;
org.freedesktop.impl.portal.Notification=gtk;
org.freedesktop.impl.portal.Secret=gnome-keyring;
+4 -4
View File
@@ -12,7 +12,7 @@ if [ -n "$SHELL" ] &&
fi
# Try to detect the service manager that is being used
if hash systemctl >/dev/null 2>&1; then
if hash systemctl &> /dev/null; then
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
@@ -41,15 +41,15 @@ if hash systemctl >/dev/null 2>&1; then
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
elif hash dinitctl >/dev/null 2>&1; then
elif hash dinitctl &> /dev/null; then
# Check that the user dinit daemon is running
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
if ! pgrep -u $(id -u) dinit &> /dev/null; then
echo "dinit user daemon is not running."
exit 1
fi
# Make sure there's no already running session.
if dinitctl --user is-started niri >/dev/null 2>&1; then
if dinitctl --user is-started niri &> /dev/null; then
echo 'A niri session is already running.'
exit 1
fi
-202
View File
@@ -1,202 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use crate::utils::get_monotonic_time;
/// Shareable lazy clock that can change rate.
///
/// The clock will fetch the time once and then retain it until explicitly cleared with
/// [`Clock::clear`].
#[derive(Debug, Default, Clone)]
pub struct Clock {
inner: Rc<RefCell<AdjustableClock>>,
}
#[derive(Debug, Default)]
struct LazyClock {
time: Option<Duration>,
}
/// Clock that can adjust its rate.
#[derive(Debug)]
struct AdjustableClock {
inner: LazyClock,
current_time: Duration,
last_seen_time: Duration,
rate: f64,
complete_instantly: bool,
}
impl Clock {
/// Creates a new clock with the given time.
pub fn with_time(time: Duration) -> Self {
let clock = AdjustableClock::new(LazyClock::with_time(time));
Self {
inner: Rc::new(RefCell::new(clock)),
}
}
/// Returns the current time.
pub fn now(&self) -> Duration {
self.inner.borrow_mut().now()
}
/// Returns the underlying time not adjusted for rate change.
pub fn now_unadjusted(&self) -> Duration {
self.inner.borrow_mut().inner.now()
}
/// Sets the unadjusted clock time.
pub fn set_unadjusted(&mut self, time: Duration) {
self.inner.borrow_mut().inner.set(time);
}
/// Clears the stored time so it's re-fetched again next.
pub fn clear(&mut self) {
self.inner.borrow_mut().inner.clear();
}
/// Gets the clock rate.
pub fn rate(&self) -> f64 {
self.inner.borrow().rate()
}
/// Sets the clock rate.
pub fn set_rate(&mut self, rate: f64) {
self.inner.borrow_mut().set_rate(rate);
}
/// Returns whether animations should complete instantly.
pub fn should_complete_instantly(&self) -> bool {
self.inner.borrow().should_complete_instantly()
}
/// Sets whether animations should complete instantly.
pub fn set_complete_instantly(&mut self, value: bool) {
self.inner.borrow_mut().set_complete_instantly(value);
}
}
impl PartialEq for Clock {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.inner, &other.inner)
}
}
impl Eq for Clock {}
impl LazyClock {
pub fn with_time(time: Duration) -> Self {
Self { time: Some(time) }
}
pub fn clear(&mut self) {
self.time = None;
}
pub fn set(&mut self, time: Duration) {
self.time = Some(time);
}
pub fn now(&mut self) -> Duration {
*self.time.get_or_insert_with(get_monotonic_time)
}
}
impl AdjustableClock {
pub fn new(mut inner: LazyClock) -> Self {
let time = inner.now();
Self {
inner,
current_time: time,
last_seen_time: time,
rate: 1.,
complete_instantly: false,
}
}
pub fn rate(&self) -> f64 {
self.rate
}
pub fn set_rate(&mut self, rate: f64) {
self.rate = rate.clamp(0., 1000.);
}
pub fn should_complete_instantly(&self) -> bool {
self.complete_instantly
}
pub fn set_complete_instantly(&mut self, value: bool) {
self.complete_instantly = value;
}
pub fn now(&mut self) -> Duration {
let time = self.inner.now();
if self.last_seen_time == time {
return self.current_time;
}
if self.last_seen_time < time {
let delta = time - self.last_seen_time;
let delta = delta.mul_f64(self.rate);
self.current_time = self.current_time.saturating_add(delta);
} else {
let delta = self.last_seen_time - time;
let delta = delta.mul_f64(self.rate);
self.current_time = self.current_time.saturating_sub(delta);
}
self.last_seen_time = time;
self.current_time
}
}
impl Default for AdjustableClock {
fn default() -> Self {
Self::new(LazyClock::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frozen_clock() {
let mut clock = Clock::with_time(Duration::ZERO);
assert_eq!(clock.now(), Duration::ZERO);
clock.set_unadjusted(Duration::from_millis(100));
assert_eq!(clock.now(), Duration::from_millis(100));
clock.set_unadjusted(Duration::from_millis(200));
assert_eq!(clock.now(), Duration::from_millis(200));
}
#[test]
fn rate_change() {
let mut clock = Clock::with_time(Duration::ZERO);
clock.set_rate(0.5);
clock.set_unadjusted(Duration::from_millis(100));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(100));
assert_eq!(clock.now(), Duration::from_millis(50));
clock.set_unadjusted(Duration::from_millis(200));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(200));
assert_eq!(clock.now(), Duration::from_millis(100));
clock.set_unadjusted(Duration::from_millis(150));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(150));
assert_eq!(clock.now(), Duration::from_millis(75));
clock.set_rate(2.0);
clock.set_unadjusted(Duration::from_millis(250));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(250));
assert_eq!(clock.now(), Duration::from_millis(275));
}
}
+102 -81
View File
@@ -2,12 +2,14 @@ use std::time::Duration;
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
mod clock;
pub use clock::Clock;
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug, Clone)]
pub struct Animation {
@@ -21,7 +23,7 @@ pub struct Animation {
/// Best effort; not always exactly precise.
clamped_duration: Duration,
start_time: Duration,
clock: Clock,
current_time: Duration,
kind: Kind,
}
@@ -46,17 +48,11 @@ pub enum Curve {
}
impl Animation {
pub fn new(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
) -> Self {
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / clock.rate().max(0.001);
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
rv.is_off = true;
return rv;
@@ -75,6 +71,7 @@ impl Animation {
}
let start_time = self.start_time;
let current_time = self.current_time;
match config.kind {
niri_config::AnimationKind::Spring(p) => {
@@ -86,11 +83,10 @@ impl Animation {
initial_velocity: self.initial_velocity,
params,
};
*self = Self::spring(self.clock.clone(), spring);
*self = Self::spring(spring);
}
niri_config::AnimationKind::Easing(p) => {
*self = Self::ease(
self.clock.clone(),
self.from,
self.to,
self.initial_velocity,
@@ -101,6 +97,7 @@ impl Animation {
}
self.start_time = start_time;
self.current_time = current_time;
}
/// Restarts the animation using the previous config.
@@ -109,12 +106,11 @@ impl Animation {
return self.clone();
}
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
match self.kind {
Kind::Easing { curve } => Self::ease(
self.clock.clone(),
from,
to,
initial_velocity,
@@ -123,37 +119,28 @@ impl Animation {
),
Kind::Spring(spring) => {
let spring = Spring {
from,
to,
from: self.from,
to: self.to,
initial_velocity: self.initial_velocity,
params: spring.params,
};
Self::spring(self.clock.clone(), spring)
Self::spring(spring)
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let threshold = 0.001; // FIXME
Self::decelerate(
self.clock.clone(),
from,
initial_velocity,
deceleration_rate,
threshold,
)
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
}
}
}
pub fn ease(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
duration_ms: u64,
curve: Curve,
) -> Self {
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
@@ -165,15 +152,19 @@ impl Animation {
duration,
// Our current curves never overshoot.
clamped_duration: duration,
start_time: clock.now(),
clock,
start_time: now,
current_time: now,
kind,
}
}
pub fn spring(clock: Clock, spring: Spring) -> Self {
pub fn spring(spring: Spring) -> Self {
let _span = tracy_client::span!("Animation::spring");
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
let kind = Kind::Spring(spring);
@@ -185,19 +176,22 @@ impl Animation {
is_off: false,
duration,
clamped_duration,
start_time: clock.now(),
clock,
start_time: now,
current_time: now,
kind,
}
}
pub fn decelerate(
clock: Clock,
from: f64,
initial_velocity: f64,
deceleration_rate: f64,
threshold: f64,
) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration_s = if initial_velocity == 0. {
0.
} else {
@@ -220,43 +214,85 @@ impl Animation {
is_off: false,
duration,
clamped_duration: duration,
start_time: clock.now(),
clock,
start_time: now,
current_time: now,
kind,
}
}
pub fn is_done(&self) -> bool {
if self.clock.should_complete_instantly() {
return true;
pub fn set_current_time(&mut self, time: Duration) {
if self.duration.is_zero() {
self.current_time = time;
return;
}
self.clock.now() >= self.start_time + self.duration
let end_time = self.start_time + self.duration;
if end_time <= self.current_time {
return;
}
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
if slowdown <= f64::EPSILON {
// Zero slowdown will cause the animation to end right away.
self.current_time = end_time;
return;
}
// We can't change current_time (since the incoming time values are always real-time), so
// apply the slowdown by shifting the start time to compensate.
if self.current_time <= time {
let delta = time - self.current_time;
let max_delta = end_time - self.current_time;
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
if slowdown <= min_slowdown {
// Our slowdown value will cause the animation to end right away.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time -= adjusted_delta - delta;
} else {
self.start_time += delta - adjusted_delta;
}
} else {
let delta = self.current_time - time;
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
if slowdown <= min_slowdown {
// Current time was about to jump to before the animation had started; let's just
// cancel the animation in this case.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time += adjusted_delta - delta;
} else {
self.start_time -= delta - adjusted_delta;
}
}
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn is_clamped_done(&self) -> bool {
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.clamped_duration
self.current_time >= self.start_time + self.clamped_duration
}
pub fn value_at(&self, at: Duration) -> f64 {
if at <= self.start_time {
// Return from when at == start_time so that when the animations are off, the behavior
// within a single event loop cycle (i.e. no time had passed since the start of an
// animation) matches the behavior when the animations are on.
return self.from;
} else if self.start_time + self.duration <= at {
pub fn value(&self) -> f64 {
if self.is_done() {
return self.to;
}
if self.clock.should_complete_instantly() {
return self.to;
}
let passed = at.saturating_sub(self.start_time);
let passed = self.current_time.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
@@ -289,10 +325,6 @@ impl Animation {
}
}
pub fn value(&self) -> f64 {
self.value_at(self.clock.now())
}
/// Returns a value that stops at the target value after first reaching it.
///
/// Best effort; not always exactly precise.
@@ -308,22 +340,11 @@ impl Animation {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
pub fn start_time(&self) -> Duration {
self.start_time
}
pub fn end_time(&self) -> Duration {
self.start_time + self.duration
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn offset(&mut self, offset: f64) {
self.from += offset;
self.to += offset;
-41
View File
@@ -54,10 +54,6 @@ impl Spring {
return Duration::MAX;
}
if (self.to - self.from).abs() <= f64::EPSILON {
return Duration::ZERO;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
@@ -94,12 +90,6 @@ impl Spring {
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
// Overdamped springs have some numerical stability issues...
if !y1.is_finite() {
return Duration::from_secs_f64(x0);
}
i += 1;
}
@@ -176,34 +166,3 @@ impl Spring {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overdamped_spring_equal_from_to_nan() {
let spring = Spring {
from: 0.,
to: 0.,
initial_velocity: 0.,
params: SpringParams::new(1.15, 850., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
#[test]
fn overdamped_spring_duration_panic() {
let spring = Spring {
from: 0.,
to: 1.,
initial_velocity: 0.,
params: SpringParams::new(6., 1200., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
}
-140
View File
@@ -1,140 +0,0 @@
//! Headless backend for tests.
//!
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
//! crucial parts like rendering.
use std::mem;
use std::sync::{Arc, Mutex};
use niri_config::OutputName;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::RenderElementStates;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::utils::Size;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Headless {
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Headless {
pub fn new() -> Self {
Self {
ipc_outputs: Default::default(),
}
}
pub fn init(&mut self, _niri: &mut Niri) {}
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
let connector = format!("headless-{n}");
let make = "niri".to_string();
let model = "headless".to_string();
let serial = n.to_string();
let output = Output::new(
connector.clone(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: make.clone(),
model: model.clone(),
},
);
let mode = Mode {
size: Size::from((i32::from(size.0), i32::from(size.1))),
refresh: 60_000,
};
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
output.user_data().insert_if_missing(|| OutputName {
connector,
make: Some(make),
model: Some(model),
serial: Some(serial),
});
let physical_properties = output.physical_properties();
self.ipc_outputs.lock().unwrap().insert(
OutputId::next(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
serial: None,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: size.0,
height: size.1,
refresh_rate: 60_000,
is_preferred: true,
}],
current_mode: Some(0),
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
},
);
niri.add_output(output, None, false);
}
pub fn seat_name(&self) -> String {
"headless".to_owned()
}
pub fn with_primary_renderer<T>(
&mut self,
_f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
None
}
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
let states = RenderElementStates::default();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
get_monotonic_time(),
Refresh::Unknown,
0,
wp_presentation_feedback::Kind::empty(),
);
let output_state = niri.output_state.get_mut(output).unwrap();
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
}
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
// FIXME: request redraw on unfinished animations remain
RenderResult::Submitted
}
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
unimplemented!()
}
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
}
impl Default for Headless {
fn default() -> Self {
Self::new()
}
}
+11 -37
View File
@@ -2,12 +2,12 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, ModKey};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
@@ -17,14 +17,9 @@ pub use tty::Tty;
pub mod winit;
pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
#[allow(clippy::large_enum_variant)]
pub enum Backend {
Tty(Tty),
Winit(Winit),
Headless(Headless),
}
#[derive(PartialEq, Eq)]
@@ -59,7 +54,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.init(niri),
Backend::Winit(winit) => winit.init(niri),
Backend::Headless(headless) => headless.init(niri),
}
}
@@ -67,7 +61,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.seat_name(),
Backend::Winit(winit) => winit.seat_name(),
Backend::Headless(headless) => headless.seat_name(),
}
}
@@ -78,7 +71,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.with_primary_renderer(f),
Backend::Winit(winit) => winit.with_primary_renderer(f),
Backend::Headless(headless) => headless.with_primary_renderer(f),
}
}
@@ -91,20 +83,13 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output),
Backend::Headless(headless) => headless.render(niri, output),
}
}
pub fn mod_key(&self, config: &Config) -> ModKey {
pub fn mod_key(&self) -> CompositorMod {
match self {
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
if let Some(ModKey::Alt) = config.input.mod_key {
ModKey::Super
} else {
ModKey::Alt
}
}),
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
}
}
@@ -112,7 +97,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.change_vt(vt),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -120,7 +104,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.suspend(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -128,7 +111,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.toggle_debug_tint(),
Backend::Winit(winit) => winit.toggle_debug_tint(),
Backend::Headless(_) => (),
}
}
@@ -136,7 +118,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
}
}
@@ -144,7 +125,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -152,7 +132,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
Backend::Headless(headless) => headless.ipc_outputs(),
}
}
@@ -164,7 +143,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
Backend::Headless(_) => None,
}
}
@@ -172,7 +150,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -180,7 +157,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -188,7 +164,13 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
pub fn on_debug_config_changed(&mut self) {
match self {
Backend::Tty(tty) => tty.on_debug_config_changed(),
Backend::Winit(_) => (),
}
}
@@ -215,12 +197,4 @@ impl Backend {
panic!("backend is not Winit")
}
}
pub fn headless(&mut self) -> &mut Headless {
if let Self::Headless(v) = self {
v
} else {
panic!("backend is not Headless")
}
}
}
+248 -301
View File
@@ -18,10 +18,9 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
use smithay::backend::drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
};
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLDevice, EGLDisplay};
@@ -29,7 +28,7 @@ use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
@@ -51,7 +50,6 @@ use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlob
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::presentation::Refresh;
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
@@ -63,14 +61,9 @@ use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
use crate::utils::{get_monotonic_time, logical_output};
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
Fourcc::Xrgb8888,
Fourcc::Xbgr8888,
Fourcc::Argb8888,
Fourcc::Abgr8888,
];
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
pub struct Tty {
config: Rc<RefCell<Config>>,
@@ -102,20 +95,19 @@ pub type TtyRenderer<'render> = MultiRenderer<
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
pub type TtyFrame<'render, 'frame> = MultiFrame<
'render,
'render,
'frame,
'buffer,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
type GbmDrmCompositor = DrmCompositor<
GbmAllocator<DrmDeviceFd>,
GbmFramebufferExporter<DrmDeviceFd>,
GbmDevice<DrmDeviceFd>,
(OutputPresentationFeedback, Duration),
DrmDeviceFd,
>;
@@ -125,7 +117,7 @@ pub struct OutputDevice {
render_node: DrmNode,
drm_scanner: DrmScanner,
surfaces: HashMap<crtc::Handle, Surface>,
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
output_ids: HashMap<crtc::Handle, OutputId>,
// SAFETY: drop after all the objects used with them are dropped.
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
@@ -136,13 +128,6 @@ pub struct OutputDevice {
active_leases: Vec<DrmLease>,
}
// A connected, but not necessarily enabled, crtc.
#[derive(Debug, Clone)]
pub struct CrtcInfo {
id: OutputId,
name: OutputName,
}
impl OutputDevice {
pub fn lease_request(
&self,
@@ -182,35 +167,6 @@ impl OutputDevice {
pub fn remove_lease(&mut self, lease_id: u32) {
self.active_leases.retain(|l| l.id() != lease_id);
}
pub fn known_crtc_name(
&self,
crtc: &crtc::Handle,
conn: &connector::Info,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
let conn_name = format_connector_name(conn);
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
}
let Some(info) = self.known_crtcs.get(crtc) else {
let conn_name = format_connector_name(conn);
error!("crtc for connector {conn_name} missing from known");
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
};
info.name.clone()
}
}
#[derive(Debug, Clone, Copy)]
@@ -227,6 +183,7 @@ struct Surface {
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
pending_gamma_change: Option<Option<Vec<u16>>>,
vrr_enabled: bool,
/// Tracy frame that goes from vblank to vblank.
vblank_frame: Option<tracy_client::Frame>,
/// Frame name for the VBlank frame.
@@ -447,6 +404,8 @@ impl Tty {
self.device_changed(node.dev_id(), niri);
// Apply pending gamma changes and restore our existing gamma.
//
// Also, restore our VRR.
let device = self.devices.get_mut(&node).unwrap();
for (crtc, surface) in device.surfaces.iter_mut() {
if let Some(ramp) = surface.pending_gamma_change.take() {
@@ -464,6 +423,33 @@ impl Tty {
warn!("error restoring gamma: {err:?}");
}
}
// Restore VRR.
let output = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == node && tty_state.crtc == *crtc
})
.cloned();
let Some(output) = output else {
error!("missing output for crtc: {crtc:?}");
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name.connector);
continue;
};
try_to_change_vrr(
&device.drm,
surface.connector,
*crtc,
surface,
output_state,
surface.vrr_enabled,
);
}
}
@@ -480,7 +466,7 @@ impl Tty {
self.refresh_ipc_outputs(niri);
niri.notify_activity();
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.monitors_active = true;
self.set_monitors_active(true);
niri.queue_redraw_all();
@@ -549,7 +535,7 @@ impl Tty {
}
drop(config);
niri.update_shaders();
niri.layout.update_shaders();
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats();
@@ -610,7 +596,7 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
known_crtcs: HashMap::new(),
output_ids: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
@@ -643,7 +629,6 @@ impl Tty {
}
};
let mut added = Vec::new();
let mut removed = Vec::new();
for event in scan_result {
match event {
@@ -651,79 +636,34 @@ impl Tty {
connector,
crtc: Some(crtc),
} => {
let connector_name = format_connector_name(&connector);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
debug!(
"new connector: {} \"{}\"",
&name.connector,
name.format_make_model_serial(),
);
// Assign an id to this crtc.
let id = OutputId::next();
added.push((crtc, CrtcInfo { id, name }));
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
} => {
self.connector_disconnected(niri, node, crtc);
removed.push(crtc);
}
_ => (),
}
}
for crtc in &removed {
self.connector_disconnected(niri, node, *crtc);
}
// FIXME: this is better done in connector_disconnected(), but currently we call that to
// turn off outputs temporarily, too. So we can't do this there.
let Some(device) = self.devices.get_mut(&node) else {
error!("device disappeared");
return;
};
for crtc in removed {
if device.known_crtcs.remove(&crtc).is_none() {
if device.output_ids.remove(&crtc).is_none() {
error!("output ID missing for disconnected crtc: {crtc:?}");
}
}
for (crtc, mut info) in added {
// Make/model/serial can match exactly between different physical monitors. This doesn't
// happen often, but our Layout does not support such duplicates and will panic.
//
// As a workaround, search for duplicates, and unname the new connectors if one is
// found. Connector names are always unique.
let name = &mut info.name;
let formatted = name.format_make_model_serial_or_connector();
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
if info.name.matches(&formatted) {
let connector = mem::take(&mut name.connector);
warn!(
"new connector {connector} duplicates make/model/serial \
of existing connector {}, unnaming",
info.name.connector,
);
*name = OutputName {
connector,
make: None,
model: None,
serial: None,
};
break;
}
}
// Insert it right away so next added connector will check against this one too.
let device = self.devices.get_mut(&node).unwrap();
device.known_crtcs.insert(crtc, info);
}
// This will connect any new connectors if needed, and apply other changes, such as
// connecting back the internal laptop monitor once it becomes the only monitor left.
//
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
// above.
self.on_output_config_changed(niri);
self.refresh_ipc_outputs(niri);
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -809,8 +749,7 @@ impl Tty {
let device = self.devices.get_mut(&node).context("missing device")?;
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
@@ -828,6 +767,10 @@ impl Tty {
return Ok(());
}
// This should be unique per CRTC connection, however currently we can call
// connector_connected() multiple times for turning the output off and on.
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
let config = self
.config
.borrow()
@@ -836,6 +779,11 @@ impl Tty {
.cloned()
.unwrap_or_default();
if config.off {
debug!("output is disabled in the config");
return Ok(());
}
for m in connector.modes() {
trace!("{m:?}");
}
@@ -863,6 +811,45 @@ impl Tty {
Err(err) => debug!("error setting max bpc: {err:?}"),
}
// Try to enable VRR if requested.
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
if enabled != vrr {
warn!("failed {} VRR", word);
}
vrr_enabled = enabled;
}
Err(err) => {
warn!("error {} VRR: {err:?}", word);
}
}
} else {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
// So that we can try it again later.
vrr_enabled = true;
}
}
} else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
let mut gamma_props = GammaProps::new(&device.drm, crtc)
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
.ok();
@@ -881,31 +868,6 @@ impl Tty {
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
// Try to enable VRR if requested.
match surface.vrr_supported(connector.handle()) {
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.use_vrr(vrr) {
warn!("error {} VRR: {err:?}", word);
}
}
Ok(VrrSupport::NotSupported) => {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector does not support it");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let _ = surface.use_vrr(false);
}
Err(err) => {
warn!("error querying for VRR support: {err:?}");
}
}
// Create GBM allocator.
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
@@ -932,6 +894,23 @@ impl Tty {
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
let config = self.config.borrow();
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
// Cursor planes have bugs on some systems.
let cursor_plane_gbm = if config.debug.disable_cursor_plane {
None
} else {
Some(device.gbm.clone())
};
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
let egl_context = renderer.as_ref().egl_context();
let render_formats = egl_context.dmabuf_render_formats();
@@ -970,15 +949,15 @@ impl Tty {
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
None,
Some(planes),
allocator.clone(),
GbmFramebufferExporter::new(device.gbm.clone()),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
// This is only used to pick a good internal format, so it can use the surface's render
// formats, even though we only ever render on the primary GPU.
render_formats.clone(),
device.drm.cursor_size(),
Some(device.gbm.clone()),
cursor_plane_gbm.clone(),
);
let mut compositor = match res {
@@ -996,17 +975,21 @@ impl Tty {
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
let mut planes = surface.planes().clone();
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
None,
Some(planes),
allocator,
GbmFramebufferExporter::new(device.gbm.clone()),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
Some(device.gbm.clone()),
cursor_plane_gbm,
)
.context("error creating DRM compositor")?
}
@@ -1015,6 +998,7 @@ impl Tty {
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
compositor.use_direct_scanout(!config.debug.disable_direct_scanout);
let mut dmabuf_feedback = None;
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
@@ -1043,8 +1027,6 @@ impl Tty {
}
}
let vrr_enabled = compositor.vrr_enabled();
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
@@ -1062,6 +1044,7 @@ impl Tty {
compositor,
dmabuf_feedback,
gamma_props,
vrr_enabled,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
@@ -1240,17 +1223,10 @@ impl Tty {
// Mark the last frame as submitted.
match surface.compositor.frame_submitted() {
Ok(Some((mut feedback, target_presentation_time))) => {
let refresh = match output_state.frame_clock.refresh_interval() {
Some(refresh) => {
if output_state.frame_clock.vrr() {
Refresh::Variable(refresh)
} else {
Refresh::Fixed(refresh)
}
}
None => Refresh::Unknown,
};
let refresh = output_state
.frame_clock
.refresh_interval()
.unwrap_or(Duration::ZERO);
// FIXME: ideally should be monotonically increasing for a surface.
let seq = meta.sequence as u64;
let mut flags = wp_presentation_feedback::Kind::Vsync
@@ -1400,35 +1376,9 @@ impl Tty {
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
let flags = {
let debug = &self.config.borrow().debug;
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
} else {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
};
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
if debug.enable_overlay_planes {
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_direct_scanout {
flags.remove(primary_scanout_flag);
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_cursor_plane {
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
}
flags
};
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
let needs_sync = res.needs_sync()
|| self
@@ -1609,13 +1559,13 @@ impl Tty {
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
let mut ipc_outputs = HashMap::new();
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name.clone());
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1650,17 +1600,8 @@ impl Tty {
}
}
let vrr_supported = surface
.map(|surface| {
matches!(
surface.compositor.vrr_supported(connector.handle()),
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
)
})
.unwrap_or_else(|| {
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
});
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
let vrr_supported = is_vrr_capable(&device.drm, connector.handle()) == Some(true);
let vrr_enabled = surface.map_or(false, |surface| surface.vrr_enabled);
let logical = niri
.global_space
@@ -1671,12 +1612,6 @@ impl Tty {
})
.map(logical_output);
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
let id = id.unwrap_or_else(|| {
error!("crtc for connector {connector_name} missing from known");
OutputId::next()
});
let ipc_output = niri_ipc::Output {
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
@@ -1690,6 +1625,10 @@ impl Tty {
logical,
};
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
error!("output ID missing for crtc: {crtc:?}");
OutputId::next()
});
ipc_outputs.insert(id, ipc_output);
}
}
@@ -1747,17 +1686,14 @@ impl Tty {
for (&crtc, surface) in device.surfaces.iter_mut() {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
if tty_state.node == node && tty_state.crtc == crtc {
let word = if enable_vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
try_to_change_vrr(
&device.drm,
surface.connector,
crtc,
surface,
output_state,
enable_vrr,
);
self.refresh_ipc_outputs(niri);
return;
}
@@ -1775,24 +1711,6 @@ impl Tty {
}
self.update_output_config_on_resume = false;
// Figure out if we should disable laptop panels.
let mut disable_laptop_panels = false;
if niri.is_lid_closed {
let config = self.config.borrow();
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
// Check if any external monitor is connected.
'outer: for device in self.devices.values() {
for (connector, _crtc) in device.drm_scanner.crtcs() {
if !is_laptop_panel(&format_connector_name(connector)) {
disable_laptop_panels = true;
break 'outer;
}
}
}
}
}
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
let mut to_disconnect = vec![];
let mut to_connect = vec![];
@@ -1805,7 +1723,7 @@ impl Tty {
.find(&surface.name)
.cloned()
.unwrap_or_default();
if config.off || should_disable(&surface.name.connector) {
if config.off {
to_disconnect.push((node, crtc));
continue;
}
@@ -1823,11 +1741,8 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
let vrr_enabled = surface.compositor.vrr_enabled();
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
let is_on_demand_vrr = config.is_vrr_on_demand();
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1849,20 +1764,17 @@ impl Tty {
continue;
};
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
|| (!is_on_demand_vrr && change_always_vrr)
{
let vrr = !vrr_enabled;
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
try_to_change_vrr(
&device.drm,
connector.handle(),
crtc,
surface,
output_state,
!surface.vrr_enabled,
);
}
if change_mode {
@@ -1894,17 +1806,12 @@ impl Tty {
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output_state.frame_clock = FrameClock::new(
Some(refresh_interval(mode)),
surface.compositor.vrr_enabled(),
);
output_state.frame_clock =
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
niri.output_resized(&output);
}
}
let config = self.config.borrow();
let disable_monitor_names = config.debug.disable_monitor_names;
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
@@ -1912,24 +1819,22 @@ impl Tty {
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
if device.surfaces.contains_key(&crtc) {
continue;
}
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let config = config
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let config = self
.config
.borrow()
.outputs
.find(&output_name)
.cloned()
.unwrap_or_default();
if !(config.off || should_disable(&output_name.connector)) {
to_connect.push((node, connector.clone(), crtc, output_name));
if !config.off {
to_connect.push((node, connector.clone(), crtc));
}
}
}
@@ -1938,11 +1843,7 @@ impl Tty {
self.connector_disconnected(niri, node, crtc);
}
// Sort by output name to get more predictable first focused output at initial compositor
// startup, when multiple connectors appear at once.
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
for (node, connector, crtc, _name) in to_connect {
for (node, connector, crtc) in to_connect {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
@@ -1951,12 +1852,24 @@ impl Tty {
self.refresh_ipc_outputs(niri);
}
pub fn on_debug_config_changed(&mut self) {
let config = self.config.borrow();
let debug = &config.debug;
let use_direct_scanout = !debug.disable_direct_scanout;
// FIXME: reload other flags if possible?
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
surface.compositor.use_direct_scanout(use_direct_scanout);
}
}
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
@@ -1965,15 +1878,12 @@ impl Tty {
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
if device.surfaces.contains_key(&crtc) {
continue;
}
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
if output_name.matches(target) {
return Some(output_name);
}
@@ -2168,8 +2078,9 @@ fn surface_dmabuf_feedback(
let surface = compositor.surface();
let planes = surface.planes();
let primary_plane_formats = surface.plane_info().formats.clone();
let primary_or_overlay_plane_formats = primary_plane_formats
let plane_formats = surface
.plane_info()
.formats
.iter()
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
.copied()
@@ -2177,11 +2088,7 @@ fn surface_dmabuf_feedback(
// We limit the scan-out trache to formats we can also render from so that there is always a
// fallback render path available in case the supplied buffer can not be scanned out directly.
let mut primary_scanout_formats = primary_plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
let mut scanout_formats = plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
@@ -2189,32 +2096,17 @@ fn surface_dmabuf_feedback(
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
// buffers produce a glitched scanout if the modifier is not Linear...
if primary_render_node != surface_render_node {
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
scanout_formats.retain(|f| f.modifier == Modifier::Linear);
}
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
trace!(
"primary scanout formats: {}, overlay adds: {}",
primary_scanout_formats.len(),
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
);
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
// increase the chance of scanning out a client even with our disabled-by-default overlay
// planes.
let scanout = builder
.clone()
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
primary_scanout_formats,
)
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
primary_or_overlay_scanout_formats,
scanout_formats,
)
.build()?;
@@ -2477,6 +2369,24 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
info.value_type().convert_value(value).as_boolean()
}
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
let (prop, info, _) =
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
device
.set_property(crtc, prop, value.into())
.context("error setting VRR_ENABLED property")?;
let value = get_drm_property(device, crtc, prop)
.context("VRR_ENABLED property missing after setting")?;
match info.value_type().convert_value(value) {
property::Value::UnsignedRange(value) => Ok(value == 1),
property::Value::Boolean(value) => Ok(value),
_ => bail!("wrong VRR_ENABLED property type"),
}
}
pub fn set_gamma_for_crtc(
device: &DrmDevice,
crtc: crtc::Handle,
@@ -2522,6 +2432,43 @@ pub fn set_gamma_for_crtc(
Ok(())
}
fn try_to_change_vrr(
device: &DrmDevice,
connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
enable_vrr: bool,
) {
let _span = tracy_client::span!("try_to_change_vrr");
if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name.connector
);
}
}
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
+11 -13
View File
@@ -3,6 +3,7 @@ use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
@@ -15,7 +16,6 @@ use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::Window;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
@@ -156,7 +156,7 @@ impl Winit {
}
drop(config);
niri.update_shaders();
niri.layout.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
@@ -190,16 +190,12 @@ impl Winit {
}
// Hand them over to winit.
let res = {
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
// FIXME: currently impossible to call due to a mutable borrow.
//
// let age = self.backend.buffer_age().unwrap();
let age = 0;
self.damage_tracker
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
.unwrap()
};
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
let res = self
.damage_tracker
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
.unwrap();
niri.update_primary_scanout_output(output, &res.states);
@@ -220,9 +216,11 @@ impl Winit {
self.backend.submit(Some(damage)).unwrap();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
let mode = output.current_mode().unwrap();
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
get_monotonic_time(),
Refresh::Unknown,
refresh,
0,
wp_presentation_feedback::Kind::empty(),
);
-11
View File
@@ -2,7 +2,6 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -55,8 +54,6 @@ pub enum Sub {
},
/// Cause a panic to check if the backtraces are good.
Panic,
/// Generate shell completions.
Completions { shell: Shell },
}
#[derive(Subcommand)]
@@ -67,18 +64,12 @@ pub enum Msg {
Workspaces,
/// List open windows.
Windows,
/// List open layer-shell surfaces.
Layers,
/// Get the configured keyboard layouts.
KeyboardLayouts,
/// Print information about the focused output.
FocusedOutput,
/// Print information about the focused window.
FocusedWindow,
/// Pick a window with the mouse and print information about it.
PickWindow,
/// Pick a color from the screen with the mouse.
PickColor,
/// Perform an action.
Action {
#[command(subcommand)]
@@ -105,6 +96,4 @@ pub enum Msg {
Version,
/// Request an error from the running niri instance.
RequestError,
/// Print the overview state.
OverviewState,
}
+15 -17
View File
@@ -4,11 +4,12 @@ use std::env;
use std::fs::File;
use std::io::Read;
use std::rc::Rc;
use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
use smithay::wayland::compositor::with_states;
@@ -66,7 +67,7 @@ impl CursorManager {
let hotspot = with_states(&surface, |states| {
states
.data_map
.get::<CursorImageSurfaceData>()
.get::<Mutex<CursorImageAttributes>>()
.unwrap()
.lock()
.unwrap()
@@ -75,24 +76,21 @@ impl CursorManager {
RenderCursor::Surface { hotspot, surface }
}
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
CursorImageStatus::Named(icon) => self
.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
}),
}
}
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
self.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
})
}
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
match &self.current_cursor {
CursorImageStatus::Hidden => false,
+4 -5
View File
@@ -6,10 +6,9 @@ use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::message::Header;
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{interface, Task};
use zbus::{dbus_interface, MessageHeader, Task};
use super::Start;
@@ -21,11 +20,11 @@ pub struct ScreenSaver {
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[interface(name = "org.freedesktop.ScreenSaver")]
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: Header<'_>,
#[zbus(header)] hdr: MessageHeader<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
@@ -34,7 +33,7 @@ impl ScreenSaver {
hdr.sender()
);
let Some(name) = hdr.sender() else {
let Ok(Some(name)) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
+4 -5
View File
@@ -1,9 +1,8 @@
use std::collections::HashMap;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{SerializeDict, Type, Value};
use zbus::{dbus_interface, SignalContext};
use super::Start;
@@ -34,7 +33,7 @@ pub struct WindowProperties {
pub app_id: String,
}
#[interface(name = "org.gnome.Shell.Introspect")]
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
impl Introspect {
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
@@ -53,8 +52,8 @@ impl Introspect {
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
// needed for the event stream IPC anyway).
#[zbus(signal)]
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl Introspect {
+2 -34
View File
@@ -1,10 +1,7 @@
use std::collections::HashMap;
use std::path::PathBuf;
use niri_ipc::PickedColor;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
use zbus::zvariant::OwnedValue;
use zbus::{interface, zvariant};
use super::Start;
@@ -15,14 +12,13 @@ pub struct Screenshot {
pub enum ScreenshotToNiri {
TakeScreenshot { include_cursor: bool },
PickColor(async_channel::Sender<Option<PickedColor>>),
}
pub enum NiriToScreenshot {
ScreenshotResult(Option<PathBuf>),
}
#[interface(name = "org.gnome.Shell.Screenshot")]
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
impl Screenshot {
async fn screenshot(
&self,
@@ -51,34 +47,6 @@ impl Screenshot {
Ok((true, filename))
}
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
let (tx, rx) = async_channel::bounded(1);
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
warn!("error sending pick color message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
let color = match rx.recv().await {
Ok(Some(color)) => color,
Ok(None) => {
return Err(fdo::Error::Failed("no color picked".to_owned()));
}
Err(err) => {
warn!("error receiving message from niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
};
let mut result = HashMap::new();
let [r, g, b] = color.rgb;
result.insert(
"color".to_string(),
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
);
Ok(result)
}
}
impl Screenshot {
+6 -31
View File
@@ -1,5 +1,5 @@
use zbus::blocking::Connection;
use zbus::object_server::Interface;
use zbus::Interface;
use crate::niri::State;
@@ -45,39 +45,12 @@ impl DBusServers {
let mut dbus = Self::default();
if is_session_instance {
let (to_niri, from_service_channel) = calloop::channel::channel();
let service_channel = ServiceChannel::new(to_niri);
niri.event_loop
.insert_source(from_service_channel, move |event, _, state| match event {
calloop::channel::Event::Msg(new_client) => {
state.niri.insert_client(new_client);
}
calloop::channel::Event::Closed => (),
})
.unwrap();
let service_channel = ServiceChannel::new(niri.display_handle.clone());
dbus.conn_service_channel = try_start(service_channel);
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let (to_niri, from_display_config) = calloop::channel::channel();
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
niri.event_loop
.insert_source(from_display_config, move |event, _, state| match event {
calloop::channel::Event::Msg(new_conf) => {
for (name, conf) in new_conf {
state.modify_output_config(&name, move |output| {
if let Some(new_output) = conf {
*output = new_output;
} else {
output.off = true;
}
});
}
state.reload_output_config();
}
calloop::channel::Event::Closed => (),
})
.unwrap();
let display_config = DisplayConfig::new(backend.ipc_outputs());
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
@@ -110,7 +83,7 @@ impl DBusServers {
dbus.conn_introspect = try_start(introspect);
#[cfg(feature = "xdp-gnome-screencast")]
{
if niri.pipewire.is_some() {
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -122,6 +95,8 @@ impl DBusServers {
.unwrap();
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}
}
+81 -209
View File
@@ -1,21 +1,15 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use smithay::utils::Size;
use serde::Serialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{fdo, interface};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
use crate::utils::is_laptop_panel;
use crate::utils::scale::supported_scales;
pub struct DisplayConfig {
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -48,18 +42,7 @@ pub struct LogicalMonitor {
properties: HashMap<String, OwnedValue>,
}
// ApplyMonitorsConfig
#[derive(Deserialize, Type)]
pub struct LogicalMonitorConfiguration {
x: i32,
y: i32,
scale: f64,
transform: u32,
_is_primary: bool,
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
}
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
&self,
@@ -70,70 +53,75 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let mut monitors = Vec::new();
let mut logical_monitors = Vec::new();
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.values()
// Take only enabled outputs.
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
.map(|output| {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
let display_name = make_display_name(output, is_laptop_panel);
for output in self.ipc_outputs.lock().unwrap().values() {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let refresh = refresh_rate as f64 / 1000.;
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let width = i32::from(width);
let height = i32::from(height);
let refresh_rate = refresh_rate as f64 / 1000.;
Mode {
id: format!("{width}x{height}@{refresh_rate:.3}"),
width,
height,
refresh_rate,
preferred_scale: 1.,
supported_scales: supported_scales(Size::from((width, height))).collect(),
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
if let Some(mode) = output.current_mode {
modes[mode]
Mode {
id: format!("{width}x{height}@{refresh:.3}"),
width: i32::from(width),
height: i32::from(height),
refresh_rate: refresh,
preferred_scale: 1.,
supported_scales: vec![1., 2., 3.],
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
modes[output.current_mode.unwrap()]
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
}
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let names = (connector, make, model, serial);
let monitor = Monitor {
names: (connector, make, model, serial),
modes,
properties,
};
let logical = output.logical.as_ref().unwrap();
if let Some(logical) = output.logical.as_ref() {
let transform = match logical.transform {
niri_ipc::Transform::Normal => 0,
niri_ipc::Transform::_90 => 1,
@@ -145,151 +133,35 @@ impl DisplayConfig {
niri_ipc::Transform::Flipped270 => 7,
};
logical_monitors.push(LogicalMonitor {
let logical_monitor = LogicalMonitor {
x: logical.x,
y: logical.y,
scale: logical.scale,
transform,
is_primary: false,
monitors: vec![names.clone()],
monitors: vec![monitor.names.clone()],
properties: HashMap::new(),
});
}
};
monitors.push(Monitor {
names,
modes,
properties,
});
}
(monitor, logical_monitor)
})
.collect();
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
async fn apply_monitors_config(
&self,
_serial: u32,
method: u32,
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
_properties: HashMap<String, OwnedValue>,
) -> fdo::Result<()> {
let current_conf = self.ipc_outputs.lock().unwrap();
let mut new_conf = HashMap::new();
for requested_config in logical_monitor_configs {
if requested_config.monitors.len() > 1 {
return Err(zbus::fdo::Error::Failed(
"Mirroring is not yet supported".to_owned(),
));
}
for (connector, mode, _props) in requested_config.monitors {
if !current_conf.values().any(|o| o.name == connector) {
return Err(zbus::fdo::Error::Failed(format!(
"Connector '{}' not found",
connector
)));
}
new_conf.insert(
connector.clone(),
Some(niri_config::Output {
off: false,
name: connector,
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
transform: match requested_config.transform {
0 => niri_ipc::Transform::Normal,
1 => niri_ipc::Transform::_90,
2 => niri_ipc::Transform::_180,
3 => niri_ipc::Transform::_270,
4 => niri_ipc::Transform::Flipped,
5 => niri_ipc::Transform::Flipped90,
6 => niri_ipc::Transform::Flipped180,
7 => niri_ipc::Transform::Flipped270,
x => {
return Err(zbus::fdo::Error::Failed(format!(
"Unknown transform {}",
x
)))
}
},
position: Some(niri_config::Position {
x: requested_config.x,
y: requested_config.y,
}),
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
zbus::fdo::Error::Failed(format!(
"Could not parse mode '{}': {}",
mode, e
))
})?),
// FIXME: VRR
..Default::default()
}),
);
}
}
if new_conf.is_empty() {
return Err(zbus::fdo::Error::Failed(
"At least one output must be enabled".to_owned(),
));
}
for output in current_conf.values() {
if !new_conf.contains_key(&output.name) {
new_conf.insert(output.name.clone(), None);
}
}
if method == 0 {
// 0 means "verify", so don't actually apply here
return Ok(());
}
if let Err(err) = self.to_niri.send(new_conf) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(())
}
#[zbus(signal)]
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[zbus(property)]
fn power_save_mode(&self) -> i32 {
-1
}
#[zbus(property)]
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
Err(zbus::Error::Unsupported)
}
#[zbus(property)]
fn panel_orientation_managed(&self) -> bool {
false
}
#[zbus(property)]
fn apply_monitors_config_allowed(&self) -> bool {
true
}
#[zbus(property)]
fn night_light_supported(&self) -> bool {
false
}
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
pub fn new(
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
) -> Self {
Self {
to_niri,
ipc_outputs,
}
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
}
}
@@ -339,16 +211,16 @@ fn format_diagonal(diagonal_inches: f64) -> String {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use k9::snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
assert_snapshot!(format_diagonal(12.11), @"12.1″");
assert_snapshot!(format_diagonal(13.28), @"13.3″");
assert_snapshot!(format_diagonal(15.6), @"15.6″");
assert_snapshot!(format_diagonal(23.2), @"23″");
assert_snapshot!(format_diagonal(24.8), @"25″");
snapshot!(format_diagonal(12.11), "12.1″");
snapshot!(format_diagonal(13.28), "13.3″");
snapshot!(format_diagonal(15.6), "15.6″");
snapshot!(format_diagonal(23.2), "23″");
snapshot!(format_diagonal(24.8), "25″");
}
}
+26 -43
View File
@@ -5,9 +5,8 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::{InterfaceRef, SignalEmitter};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{fdo, interface, ObjectServer};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
@@ -62,8 +61,6 @@ static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct Stream {
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
@@ -95,17 +92,16 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
stream_id: usize,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalEmitter<'static>,
signal_ctx: SignalContext<'static>,
},
StopCast {
session_id: usize,
},
}
#[interface(name = "org.gnome.Mutter.ScreenCast")]
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
impl ScreenCast {
async fn create_session(
&self,
@@ -140,26 +136,26 @@ impl ScreenCast {
Ok(path)
}
#[zbus(property)]
#[dbus_interface(property)]
async fn version(&self) -> i32 {
4
}
}
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
impl Session {
async fn start(&self) {
debug!("start");
for (stream, iface) in &*self.streams.lock().unwrap() {
stream.start(iface.signal_emitter().clone());
stream.start(self.id, iface.signal_context().clone());
}
}
pub async fn stop(
&self,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
) {
debug!("stop");
@@ -179,7 +175,7 @@ impl Session {
let streams = mem::take(&mut *self.streams.lock().unwrap());
for (_, iface) in streams.iter() {
server
.remove::<Stream, _>(iface.signal_emitter().path())
.remove::<Stream, _>(iface.signal_context().path())
.await
.unwrap();
}
@@ -207,20 +203,16 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let target = StreamTarget::Output(output);
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -244,8 +236,10 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(?properties, "record_window");
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
@@ -253,13 +247,7 @@ impl Session {
let target = StreamTarget::Window {
id: properties.window_id,
};
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -276,17 +264,17 @@ impl Session {
Ok(path)
}
#[zbus(signal)]
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[dbus_interface(signal)]
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
impl Stream {
#[zbus(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
#[zbus(property)]
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
match &self.target {
StreamTarget::Output(output) => {
@@ -361,15 +349,11 @@ impl Drop for Session {
impl Stream {
fn new(
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
session_id,
target,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
@@ -377,14 +361,13 @@ impl Stream {
}
}
fn start(&self, ctxt: SignalEmitter<'static>) {
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
let msg = ScreenCastToNiri::StartCast {
session_id: self.session_id,
stream_id: self.id,
session_id,
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
+19 -20
View File
@@ -1,51 +1,50 @@
use std::os::fd::{FromRawFd, IntoRawFd};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use zbus::{fdo, interface, zvariant};
use smithay::reexports::wayland_server::DisplayHandle;
use zbus::dbus_interface;
use super::Start;
use crate::niri::NewClient;
use crate::niri::ClientState;
pub struct ServiceChannel {
to_niri: calloop::channel::Sender<NewClient>,
display: DisplayHandle,
}
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
impl ServiceChannel {
async fn open_wayland_service_connection(
&mut self,
service_client_type: u32,
) -> fdo::Result<zvariant::OwnedFd> {
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
if service_client_type != 1 {
return Err(fdo::Error::InvalidArgs(
return Err(zbus::fdo::Error::InvalidArgs(
"Invalid service client type".to_owned(),
));
}
let (sock1, sock2) = UnixStream::pair().unwrap();
let client = NewClient {
client: sock2,
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
restricted: false,
// FIXME: maybe you can get the PID from D-Bus somehow?
credentials_unknown: true,
};
if let Err(err) = self.to_niri.send(client) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
});
self.display.insert_client(sock2, data).unwrap();
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
}
}
impl ServiceChannel {
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
Self { to_niri }
pub fn new(display: DisplayHandle) -> Self {
Self { display }
}
}
impl Start for ServiceChannel {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::connection::Builder::session()?
let conn = zbus::blocking::ConnectionBuilder::session()?
.name("org.gnome.Mutter.ServiceChannel")?
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
.build()?;
+63 -125
View File
@@ -1,7 +1,6 @@
use std::collections::hash_map::Entry;
use niri_ipc::PositionChange;
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
@@ -19,11 +18,9 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
use crate::layout::{ActivateWindow, AddWindowTarget};
use crate::niri::{CastTarget, ClientState, LockState, State};
use crate::niri::{ClientState, State};
use crate::utils::send_scale_transform;
use crate::utils::transaction::Transaction;
use crate::utils::{is_mapped, send_scale_transform};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
@@ -78,25 +75,25 @@ impl CompositorHandler for State {
if surface == &root_surface {
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
if is_mapped(surface) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
// The toplevel got mapped.
let Unmapped {
window,
state,
activation_token_data,
} = entry.remove();
let Unmapped { window, state } = entry.remove();
window.on_commit();
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, height, is_full_width, output, workspace_id) =
let (rules, width, is_full_width, output, workspace_name) =
if let InitialConfigureState::Configured {
rules,
width,
height,
floating_width: _,
floating_height: _,
is_full_width,
output,
workspace_name,
@@ -107,48 +104,15 @@ impl CompositorHandler for State {
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
// Check that the workspace still exists.
let workspace_id = workspace_name
.as_deref()
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
.map(|(_, ws)| ws.id());
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
(rules, width, height, is_full_width, output, workspace_id)
(rules, width, is_full_width, output, workspace_name)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, None, false, None, None)
(ResolvedWindowRules::empty(), None, false, None, None)
};
// The GTK about dialog sets min/max size after the initial configure but
// before mapping, so we need to compute open_floating at the last possible
// moment, that is here.
let is_floating = rules.compute_open_floating(toplevel);
// Figure out if we should activate the window.
let activate = rules.open_focused.map(|focus| {
if focus {
ActivateWindow::Yes
} else {
ActivateWindow::No
}
});
let activate = activate.unwrap_or_else(|| {
// Check the token timestamp again in case the window took a while between
// requesting activation and mapping.
let token = activation_token_data.filter(|token| {
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
});
if token.is_some() {
ActivateWindow::Yes
} else {
let config = self.niri.config.borrow();
if config.debug.strict_new_window_focus_policy {
ActivateWindow::No
} else {
ActivateWindow::Smart
}
}
});
let parent = toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
@@ -159,9 +123,7 @@ impl CompositorHandler for State {
// None. If the configured output is set, that means it was set explicitly
// by a window rule or a fullscreen request.
.filter(|(_, parent_output)| {
parent_output.is_none()
|| output.is_none()
|| output.as_ref() == *parent_output
output.is_none() || output.as_ref() == Some(*parent_output)
})
.map(|(mapped, _)| mapped.window.clone());
@@ -171,34 +133,34 @@ impl CompositorHandler for State {
let mapped = Mapped::new(window, rules, hook);
let window = mapped.window.clone();
let target = if let Some(p) = &parent {
// Open dialogs next to their parent window.
AddWindowTarget::NextTo(p)
} else if let Some(id) = workspace_id {
AddWindowTarget::Workspace(id)
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(workspace_name) = &workspace_name {
self.niri.layout.add_window_to_named_workspace(
workspace_name,
mapped,
width,
is_full_width,
)
} else if let Some(output) = &output {
AddWindowTarget::Output(output)
self.niri
.layout
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
} else {
AddWindowTarget::Auto
self.niri.layout.add_window(mapped, width, is_full_width)
};
let output = self.niri.layout.add_window(
mapped,
target,
width,
height,
is_full_width,
is_floating,
activate,
);
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_focus = self.niri.layout.focus().map(|m| &m.window);
if new_focus == Some(&window) {
// We activated the newly opened window.
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
}
self.niri.queue_redraw(&output);
@@ -218,12 +180,18 @@ impl CompositorHandler for State {
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.cloned();
let output = output.clone();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel.
let is_mapped = is_mapped(surface);
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
// Must start the close animation before window.on_commit().
let transaction = Transaction::new();
@@ -242,11 +210,14 @@ impl CompositorHandler for State {
// The toplevel got unmapped.
//
// Test client: wleird-unmap.
let active_window = self.niri.layout.focus().map(|m| &m.window);
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(CastTarget::Window { id: id.get() });
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: id.get(),
});
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface);
@@ -266,27 +237,18 @@ impl CompositorHandler for State {
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
self.niri.queue_redraw(&output);
return;
}
let (serial, buffer_delta) = with_states(surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
let serial = with_states(surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
(role.configure_serial, buffer_delta)
role.configure_serial
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
@@ -295,25 +257,10 @@ impl CompositorHandler for State {
// The toplevel remains mapped.
self.niri.layout.update_window(&window, serial);
// Move the toplevel according to the attach offset.
if let Some(delta) = buffer_delta {
if delta.x != 0 || delta.y != 0 {
let (x, y) = delta.to_f64().into();
self.niri.layout.move_floating_window(
Some(&window),
PositionChange::AdjustFixed(x),
PositionChange::AdjustFixed(y),
false,
);
}
}
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window);
self.update_reactive_popups(&window, &output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
self.niri.queue_redraw(&output);
return;
}
@@ -324,12 +271,10 @@ impl CompositorHandler for State {
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.cloned();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window, None);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
self.niri.queue_redraw(&output);
return;
}
@@ -404,23 +349,16 @@ impl CompositorHandler for State {
}
// This might be a lock surface.
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
self.niri.maybe_continue_to_locking();
} else {
if self.niri.is_locked() {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
self.niri.queue_redraw(&output.clone());
return;
}
return;
}
}
}
// This message can trigger for lock surfaces that had a commit right after we unlocked
// the session, but that's ok, we don't need to handle them.
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
}
fn destroyed(&mut self, surface: &WlSurface) {
+10 -50
View File
@@ -1,3 +1,4 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
@@ -10,9 +11,8 @@ use smithay::wayland::shell::wlr_layer::{
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::{is_mapped, output_size, send_scale_transform};
use crate::utils::send_scale_transform;
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -60,7 +60,6 @@ impl WlrLayerShellHandler for State {
layer.map(|layer| (o.clone(), map, layer))
}) {
map.unmap_layer(&layer);
self.niri.mapped_layer_surfaces.remove(&layer);
Some(output)
} else {
None
@@ -119,38 +118,16 @@ impl State {
.unwrap();
if initial_configure_sent {
if is_mapped(surface) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let config = self.niri.config.borrow();
let rules = &config.layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let output_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
let mapped = MappedLayer::new(
layer.clone(),
rules,
output_size,
scale,
self.niri.clock.clone(),
&config,
);
let prev = self
.niri
.mapped_layer_surfaces
.insert(layer.clone(), mapped);
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
@@ -174,24 +151,7 @@ impl State {
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
let was_mapped = self.niri.mapped_layer_surfaces.remove(layer).is_some();
self.niri.unmapped_layer_surfaces.insert(surface.clone());
// After layer surface unmaps it has to perform the initial commit-configure
// sequence again. This is a workaround until Smithay properly resets
// initial_configure_sent upon the surface unmapping itself as it does for
// toplevels.
if was_mapped {
with_states(surface, |states| {
let mut data = states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap();
data.initial_configure_sent = false;
});
}
}
} else {
let scale = output.current_scale();
+62 -207
View File
@@ -7,11 +7,10 @@ use std::io::Write;
use std::os::fd::OwnedFd;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
use smithay::backend::input::TabletToolDescriptor;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
@@ -35,9 +34,6 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::keyboard_shortcuts_inhibit::{
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
@@ -47,15 +43,10 @@ use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
};
use smithay::wayland::selection::ext_data_control::{
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
};
use smithay::wayland::selection::primary_selection::{
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
};
use smithay::wayland::selection::wlr_data_control::{
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
};
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
@@ -66,18 +57,16 @@ use smithay::wayland::xdg_activation::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
delegate_drm_lease, delegate_fractional_scale, 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_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::layout::ActivateWindow;
use crate::niri::{DndIcon, NewClient, State};
use crate::niri::{ClientState, DndIcon, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
@@ -85,19 +74,12 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::protocols::virtual_pointer::{
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
VirtualPointerMotionEvent,
};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::utils::{output_size, send_scale_transform};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
delegate_output_management, delegate_screencopy,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
@@ -155,12 +137,11 @@ impl TabletSeatHandler for State {
delegate_tablet_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
// Pointer constraints track pointer focus internally, so make sure it's up to date before
// activating a new one.
self.refresh_pointer_contents();
self.niri.maybe_activate_pointer_constraint();
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
pointer.current_location(),
&self.niri.pointer_focus,
);
}
fn cursor_position_hint(
@@ -170,27 +151,26 @@ impl PointerConstraintsHandler for State {
location: Point<f64, Logical>,
) {
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
constraint.is_some_and(|c| c.is_active())
constraint.map_or(false, |c| c.is_active())
});
if !is_constraint_active {
return;
}
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
// will change, even though the real pointer focus remains on the Blender surface due to
// the click grab.
// Logically the following two checks should always succeed (so, they should print
// error!()s if they fail). However, currently both can fail because niri's pointer focus
// doesn't take pointer grabs into account. So if you start, say, a middle-drag in Blender,
// then touchpad-swipe the window away, the niri pointer focus will change, even though the
// real pointer focus remains on the Blender surface due to the click grab.
//
// Ideally we would just use the constraint surface, but we need its origin. So this is
// more of a hack because pointer contents has the surface origin available.
//
// FIXME: use the constraint surface somehow, don't use pointer contents.
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
// FIXME: add error!()s when niri pointer focus takes grabs into account. Alternatively,
// recompute the surface origin here (but that is a bit clunky).
let Some((ref focused_surface, origin)) = self.niri.pointer_focus.surface else {
return;
};
if surface_under_pointer != surface {
if focused_surface != surface {
return;
}
@@ -211,7 +191,7 @@ impl PointerConstraintsHandler for State {
pointer.set_location(target);
// Redraw to update the cursor position if it's visible.
if self.niri.pointer_visibility.is_visible() {
if !self.niri.pointer_hidden {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
@@ -258,28 +238,7 @@ impl InputMethodHandler for State {
}
}
impl KeyboardShortcutsInhibitHandler for State {
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
&mut self.niri.keyboard_shortcuts_inhibit_state
}
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
inhibitor.activate();
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.insert(inhibitor.wl_surface().clone(), inhibitor);
}
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.remove(&inhibitor.wl_surface().clone());
}
}
delegate_input_method_manager!(State);
delegate_keyboard_shortcuts_inhibit!(State);
delegate_virtual_keyboard_manager!(State);
impl SelectionHandler for State {
@@ -342,42 +301,7 @@ impl ClientDndGrabHandler for State {
self.niri.queue_redraw_all();
}
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
trace!("client dropped, target: {target:?}, validated: {validated}");
// End DnD before activating a specific window below so that it takes precedence.
self.niri.layout.dnd_end();
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
// example. On successful drop, additionally activate the target window.
let mut activate_output = true;
if let Some(target) = validated.then_some(target).flatten() {
let root = self.niri.find_root_shell_surface(&target);
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
activate_output = false;
}
}
if activate_output {
// Find the output from cursor coordinates.
//
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
// and if it comes from touch, then what the coordinates are. Need to pass more
// parameters from Smithay I guess.
//
// Assume that hidden pointer means touch DnD.
if self.niri.pointer_visibility.is_visible() {
// We can't even get the current pointer location because it's locked (we're deep
// in the grab call stack here). So use the last known one.
if let Some(output) = &self.niri.pointer_contents.output {
self.niri.layout.focus_output(output);
}
}
}
fn dropped(&mut self, _seat: Seat<Self>) {
self.niri.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
@@ -395,22 +319,14 @@ impl PrimarySelectionHandler for State {
}
delegate_primary_selection!(State);
impl WlrDataControlHandler for State {
fn data_control_state(&self) -> &WlrDataControlState {
&self.niri.wlr_data_control_state
impl DataControlHandler for State {
fn data_control_state(&self) -> &DataControlState {
&self.niri.data_control_state
}
}
delegate_data_control!(State);
impl ExtDataControlHandler for State {
fn data_control_state(&self) -> &ExtDataControlState {
&self.niri.ext_data_control_state
}
}
delegate_ext_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
@@ -451,8 +367,6 @@ impl SessionLockHandler for State {
fn unlock(&mut self) {
self.niri.unlock();
self.niri.activate_monitors(&mut self.backend);
self.niri.notify_activity();
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
@@ -486,12 +400,18 @@ impl SecurityContextHandler for State {
self.niri
.event_loop
.insert_source(source, move |client, _, state| {
trace!("inserting a new restricted client, context={context:?}");
state.niri.insert_client(NewClient {
client,
let config = state.niri.config.borrow();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
restricted: true,
credentials_unknown: false,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
})
.unwrap();
}
@@ -539,25 +459,22 @@ impl ForeignToplevelHandler for State {
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
role.current
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
});
if !has_fullscreen_cap {
if !mapped
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
if &requested_output != current_output {
self.niri
.layout
.move_to_output(Some(&window), &requested_output, None);
}
}
@@ -602,31 +519,6 @@ impl ScreencopyHandler for State {
}
delegate_screencopy!(State);
impl VirtualPointerHandler for State {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
&mut self.niri.virtual_pointer_state
}
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
}
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
self.process_input_event(
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
);
}
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
}
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
}
}
delegate_virtual_pointer!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
self.backend
@@ -707,76 +599,41 @@ impl GammaControlHandler for State {
}
delegate_gamma_control!(State);
struct UrgentOnlyMarker;
impl XdgActivationHandler for State {
fn activation_state(&mut self) -> &mut XdgActivationState {
&mut self.niri.activation_state
}
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
// common client behavior.
//
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
// Only tokens that were created while the application has keyboard focus are valid.
let Some((serial, seat)) = data.serial else {
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
return true;
return false;
};
let Some(seat) = Seat::<State>::from_resource(&seat) else {
return false;
};
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
// upon clicking on their tray icon or on their notification. This debug flag makes that
// work.
//
// Clicking on a notification sends clients a perfectly valid activation token from the
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
// and we can remove this debug flag.
let config = self.niri.config.borrow();
if config.debug.honor_xdg_activation_with_invalid_serial {
return true;
}
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
// with no keyboard interactivity won't have any keyboard focus.
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
false
let keyboard = seat.get_keyboard().unwrap();
keyboard
.last_enter()
.map(|last_enter| serial.is_no_older_than(&last_enter))
.unwrap_or(false)
}
fn request_activation(
&mut self,
token: XdgActivationToken,
_token: XdgActivationToken,
token_data: XdgActivationTokenData,
surface: WlSurface,
) {
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&surface) {
if token_data.timestamp.elapsed().as_secs() < 10 {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
let window = mapped.window.clone();
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
mapped.set_urgent(true);
self.niri.queue_redraw_all();
} else {
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
}
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
unmapped.activation_token_data = Some(token_data);
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
}
}
self.niri.activation_state.remove_token(&token);
}
}
delegate_xdg_activation!(State);
@@ -798,5 +655,3 @@ delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
delegate_single_pixel_buffer!(State);
+92 -204
View File
@@ -1,7 +1,6 @@
use std::cell::Cell;
use calloop::Interest;
use niri_config::PresetSize;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -43,12 +42,10 @@ use crate::input::resize_grab::ResizeGrab;
use crate::input::touch_move_grab::TouchMoveGrab;
use crate::input::touch_resize_grab::TouchResizeGrab;
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
use crate::layout::ActivateWindow;
use crate::niri::{CastTarget, PopupGrabState, State};
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{
get_monotonic_time, output_matches_name, send_scale_transform, update_tiled_state, ResizeEdge,
};
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -86,7 +83,7 @@ impl XdgShellHandler for State {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
let is_dnd_grab = grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
@@ -106,7 +103,8 @@ impl XdgShellHandler for State {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
let is_dnd_grab =
grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
@@ -126,10 +124,6 @@ impl XdgShellHandler for State {
return;
};
let Some(output) = output else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
@@ -153,8 +147,9 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window, false);
let grab = MoveGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
@@ -216,16 +211,8 @@ impl XdgShellHandler for State {
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let mut last = last_cell.get();
let last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click gestures, so just allow it
// to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
@@ -260,6 +247,7 @@ impl XdgShellHandler for State {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
@@ -296,7 +284,6 @@ impl XdgShellHandler for State {
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
trace!("ignoring popup grab because no root surface");
return;
};
@@ -305,41 +292,30 @@ impl XdgShellHandler for State {
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
trace!("ignoring popup grab because the session is locked");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
trace!("ignoring popup grab because the screenshot UI is open");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if let Some(layer) = layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) {
// This is a grab for a layer surface.
if let Some(mapped) = self.niri.mapped_layer_surfaces.get(layer) {
if mapped.place_within_backdrop() {
trace!("ignoring popup grab for a layer surface within overview backdrop");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else {
// This is a grab for a regular window; check that there's no layer surface with a
// higher input priority.
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers.layers_on(Layer::Overlay).any(|l| {
(l.cached_state().keyboard_interactivity
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
&& self.niri.mapped_layer_surfaces.contains_key(l)
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
trace!("ignoring toplevel popup grab because the overlay layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -347,57 +323,38 @@ impl XdgShellHandler for State {
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers.layers_on(Layer::Top).any(|l| {
(l.cached_state().keyboard_interactivity
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
&& self.niri.mapped_layer_surfaces.contains_key(l)
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
trace!("ignoring toplevel popup grab because the top layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
trace!("ignoring toplevel popup grab because another window has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
trace!("ignoring popup grab because no output is active");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let mut grab = match self
let Ok(mut grab) = self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
{
Ok(grab) => grab,
Err(err) => {
trace!("ignoring popup grab: {err:?}");
return;
}
else {
return;
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let can_receive_keyboard_focus = self
.niri
.layout
.active_output()
.and_then(|output| {
layer_map_for_output(output)
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
})
.unwrap_or(true);
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
@@ -406,22 +363,16 @@ impl XdgShellHandler for State {
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
trace!("ignoring popup grab because of current grab mismatch");
if keyboard_grab_mismatches || pointer_grab_mismatches {
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
if can_receive_keyboard_focus {
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
}
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState {
root,
grab,
has_keyboard_grab: can_receive_keyboard_focus,
});
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
@@ -448,26 +399,23 @@ impl XdgShellHandler for State {
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
.find_window_and_output(toplevel.wl_surface())
{
// A configure is required in response to this event regardless if there are pending
// changes.
mapped.set_needs_configure();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
if &requested_output != current_output {
self.niri
.layout
.move_to_output(Some(&window), &requested_output, None);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -489,7 +437,7 @@ impl XdgShellHandler for State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.and_then(|(_win, output)| output)
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -514,7 +462,7 @@ impl XdgShellHandler for State {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None, None, false, rules);
ws.configure_new_window(&unmapped.window, None, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -531,14 +479,14 @@ impl XdgShellHandler for State {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
.find_window_and_output(toplevel.wl_surface())
{
// A configure is required in response to this event regardless if there are pending
// changes.
mapped.set_needs_configure();
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -549,9 +497,6 @@ impl XdgShellHandler for State {
InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name,
@@ -575,7 +520,7 @@ impl XdgShellHandler for State {
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.and_then(|(_win, output)| output)
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -606,26 +551,12 @@ impl XdgShellHandler for State {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let is_floating = rules.compute_open_floating(&toplevel);
let configure_width = if is_floating {
*floating_width
} else if *is_full_width {
Some(PresetSize::Proportion(1.))
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
let configure_height = if is_floating {
*floating_height
} else {
*height
};
ws.configure_new_window(
&unmapped.window,
configure_width,
configure_height,
is_floating,
rules,
);
ws.configure_new_window(&unmapped.window, configure_width, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -661,11 +592,13 @@ impl XdgShellHandler for State {
return;
};
let window = mapped.window.clone();
let output = output.cloned();
let output = output.clone();
self.niri.stop_casts_for_target(CastTarget::Window {
id: mapped.id().get(),
});
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
@@ -679,7 +612,7 @@ impl XdgShellHandler for State {
.start_close_animation_for_window(renderer, &window, blocker);
});
let active_window = self.niri.layout.focus().map(|m| &m.window);
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window, transaction.clone());
@@ -695,9 +628,7 @@ impl XdgShellHandler for State {
self.maybe_warp_cursor_to_focus();
}
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
self.niri.queue_redraw(&output);
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
@@ -713,22 +644,6 @@ impl XdgShellHandler for State {
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn parent_changed(&mut self, toplevel: ToplevelSurface) {
let Some(parent) = toplevel.parent() else {
return;
};
if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
let output = output.cloned();
let window = mapped.window.clone();
if self.niri.layout.descendants_added(&window) {
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
delegate_xdg_shell!(State);
@@ -757,13 +672,7 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if toplevel.is_initial_configure_sent() {
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
toplevel.send_configure();
}
}
@@ -776,20 +685,14 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if toplevel.is_initial_configure_sent() {
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
toplevel.send_configure();
}
}
}
delegate_xdg_decoration!(State);
/// Whether KDE server decorations are in use.
#[derive(Default, Clone)]
#[derive(Default)]
pub struct KdeDecorationsModeState {
server: Cell<bool>,
}
@@ -852,7 +755,7 @@ impl State {
self.niri.is_at_startup,
);
let Unmapped { window, state, .. } = unmapped;
let Unmapped { window, state } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
@@ -893,7 +796,7 @@ impl State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.and_then(|(_win, output)| output)
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
@@ -914,11 +817,7 @@ impl State {
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let mut floating_width = None;
let mut height = None;
let mut floating_height = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
let is_floating = rules.compute_open_floating(toplevel);
// Tell the surface the preferred size and bounds for its likely output.
let ws = rules
@@ -940,38 +839,31 @@ impl State {
});
}
width = ws.resolve_default_width(rules.default_width, false);
floating_width = ws.resolve_default_width(rules.default_width, true);
height = ws.resolve_default_height(rules.default_height, false);
floating_height = ws.resolve_default_height(rules.default_height, true);
width = ws.resolve_default_width(rules.default_width);
let configure_width = if is_floating {
floating_width
} else if is_full_width {
Some(PresetSize::Proportion(1.))
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
let configure_height = if is_floating { floating_height } else { height };
ws.configure_new_window(
window,
configure_width,
configure_height,
is_floating,
&rules,
);
ws.configure_new_window(window, configure_width, &rules);
}
// Set the tiled state for the initial configure.
update_tiled_state(toplevel, config.prefer_no_csd, rules.tiled_state);
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name().cloned()),
@@ -1039,8 +931,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@@ -1050,10 +942,19 @@ impl State {
}
}
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) {
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = self.niri.layout.popup_target_rect(window);
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
target.loc -= get_popup_toplevel_coords(popup).to_f64();
self.position_popup_within_rect(popup, target);
@@ -1073,20 +974,7 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_size(output_geo.size);
// Background and bottom layer popups render below the top and the overlay layer, so let's
// put them into the non-exclusive zone.
//
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
// Smithay only computes the "all layers" non-exclusive zone atm.
//
// FIXME: related to the above, top layer popups should use the "overlay layer"
// non-exclusive zone.
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
target = map.non_exclusive_zone();
}
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
@@ -1131,7 +1019,7 @@ impl State {
}
}
pub fn update_reactive_popups(&self, window: &Window) {
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(
@@ -1140,7 +1028,7 @@ impl State {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(xdg_popup, window);
self.unconstrain_window_popup(xdg_popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
-51
View File
@@ -1,51 +0,0 @@
use ::input as libinput;
use smithay::backend::input;
use smithay::backend::winit::WinitVirtualDevice;
use smithay::output::Output;
use crate::niri::State;
use crate::protocols::virtual_pointer::VirtualPointer;
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
type NiriDevice: NiriInputDevice;
}
impl<T: input::InputBackend> NiriInputBackend for T
where
Self::Device: NiriInputDevice,
{
type NiriDevice = Self::Device;
}
pub trait NiriInputDevice: input::Device {
// FIXME: this should maybe be per-event, not per-device,
// but it's not clear that this matters in practice?
// it might be more obvious once we implement it for libinput
fn output(&self, state: &State) -> Option<Output>;
}
impl NiriInputDevice for libinput::Device {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: Allow specifying the output per-device?
None
}
}
impl NiriInputDevice for WinitVirtualDevice {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: we should be returning the single output that the winit backend creates,
// but for now, that will cause issues because the output is normally upside down,
// so we apply Transform::Flipped180 to it and that would also cause
// the cursor position to be flipped, which is not what we want.
//
// instead, we just return None and rely on the fact that it has only one output.
// doing so causes the cursor to be placed in *global* output coordinates,
// which are not flipped, and happen to be what we want.
None
}
}
impl NiriInputDevice for VirtualPointer {
fn output(&self, _: &State) -> Option<Output> {
self.output().cloned()
}
}
+358 -1887
View File
File diff suppressed because it is too large Load Diff
+37 -52
View File
@@ -1,11 +1,12 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -16,32 +17,16 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
is_moving: bool,
}
impl MoveGrab {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
gesture,
is_moving: false,
}
}
@@ -49,6 +34,7 @@ impl MoveGrab {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
@@ -72,24 +58,6 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
@@ -97,6 +65,14 @@ impl PointerGrab<State> for MoveGrab {
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
@@ -129,18 +105,27 @@ impl PointerGrab<State> for MoveGrab {
) {
handle.button(data, event);
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
} else {
0x110
};
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
data.niri.layout.toggle_window_floating(Some(&self.window));
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// TODO: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
if !handle.current_pressed().contains(&self.start_data.button) {
// The button that initiated the grab was released.
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
-227
View File
@@ -1,227 +0,0 @@
use niri_ipc::PickedColor;
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::ButtonState;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
use crate::niri::State;
use crate::render_helpers::{render_to_vec, RenderTarget};
pub struct PickColorGrab {
start_data: PointerGrabStartData<State>,
}
impl PickColorGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_color.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
state.niri.queue_redraw_all();
}
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
let (output, pos_within_output) = data.niri.output_under(location)?;
let output = output.clone();
data.backend
.with_primary_renderer(|renderer| {
data.niri.update_render_elements(Some(&output));
let scale = Scale::from(output.current_scale().fractional_scale());
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
// at the bottom/right edges of the monitors.
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::<i32, Physical>::from((1, 1));
let elements = data.niri.render(
renderer,
&output,
false,
// This is an interactive operation so we can render without blocking out.
RenderTarget::Output,
);
let pixels = match render_to_vec(
renderer,
size,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements.iter().rev().map(|elem| {
let offset = pos.upscale(-1);
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
}),
) {
Ok(pixels) => pixels,
Err(_) => return None,
};
if pixels.len() == 4 {
let rgb = [
f64::from(pixels[0]) / 255.0,
f64::from(pixels[1]) / 255.0,
f64::from(pixels[2]) / 255.0,
];
Some(PickedColor { rgb })
} else {
error!(
"unexpected pixel data length: {} (expected 4)",
pixels.len()
);
None
}
})
.flatten()
}
}
impl PointerGrab<State> for PickColorGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_color.take() {
let color = Self::pick_color_at_point(handle.current_location(), data);
let _ = tx.send_blocking(color);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
-173
View File
@@ -1,173 +0,0 @@
use smithay::backend::input::ButtonState;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Point};
use crate::niri::State;
use crate::window::Mapped;
pub struct PickWindowGrab {
start_data: PointerGrabStartData<State>,
}
impl PickWindowGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_window.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
// Redraw to update the cursor.
state.niri.queue_redraw_all();
}
}
impl PointerGrab<State> for PickWindowGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_window.take() {
let _ = tx.send_blocking(
data.niri
.window_under(handle.current_location())
.map(Mapped::id),
);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+1
View File
@@ -22,6 +22,7 @@ impl ResizeGrab {
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
-69
View File
@@ -1,69 +0,0 @@
//! Swipe gesture from scroll events.
//!
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
//! gesture events for 3+ fingers.
#[derive(Debug)]
pub struct ScrollSwipeGesture {
ongoing: bool,
vertical: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
BeginUpdate,
Update,
End,
}
impl ScrollSwipeGesture {
pub const fn new() -> Self {
Self {
ongoing: false,
vertical: false,
}
}
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
if dx == 0. && dy == 0. {
self.ongoing = false;
Action::End
} else if !self.ongoing {
self.ongoing = true;
self.vertical = dy != 0.;
Action::BeginUpdate
} else {
Action::Update
}
}
pub fn reset(&mut self) -> bool {
if self.ongoing {
self.ongoing = false;
true
} else {
false
}
}
pub fn is_vertical(&self) -> bool {
self.vertical
}
}
impl Default for ScrollSwipeGesture {
fn default() -> Self {
Self::new()
}
}
impl Action {
pub fn begin(self) -> bool {
self == Action::BeginUpdate
}
pub fn end(self) -> bool {
self == Action::End
}
}
+10 -29
View File
@@ -10,14 +10,12 @@ use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
workspace_id: WorkspaceId,
gesture: GestureState,
}
@@ -29,24 +27,12 @@ enum GestureState {
}
impl SpatialMovementGrab {
pub fn new(
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
Self {
last_location: start_data.location,
start_data,
output,
workspace_id,
gesture,
gesture: GestureState::Recognizing,
}
}
@@ -54,14 +40,17 @@ impl SpatialMovementGrab {
let layout = &mut state.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => None,
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(false, Some(false))
}
};
if let Some(output) = res {
state.niri.queue_redraw(&output);
}
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
@@ -93,16 +82,8 @@ impl PointerGrab<State> for SpatialMovementGrab {
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
None
}
} else {
None
}
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
@@ -125,7 +106,7 @@ impl PointerGrab<State> for SpatialMovementGrab {
data.niri.queue_redraw(&output);
}
} else {
// The move is no longer ongoing.
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
-274
View File
@@ -1,274 +0,0 @@
use std::time::Duration;
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::niri::State;
use crate::window::Mapped;
// When the touch is stationary for this much time, it becomes an interactive move.
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
pub struct TouchOverviewGrab {
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
last_location: Point<f64, Logical>,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
InteractiveMove,
}
impl TouchOverviewGrab {
pub fn new(
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
) -> Self {
Self {
last_location: start_data.location,
start_timestamp,
start_data,
output,
start_pos_within_output,
workspace_id,
workspace_matched_narrow,
window,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
match self.gesture {
GestureState::Recognizing => {
// Tap to activate.
layout.focus_output(&self.output);
// Activate the workspace if necessary.
if self.window.is_some() || self.workspace_matched_narrow {
// When activating a window, we want to activate the window's current
// workspace. Otherwise, find the workspace that we tapped on.
let ws_matches = |ws: &Workspace<Mapped>| {
if let Some(window) = &self.window {
ws.has_window(window)
} else if let Some(ws_id) = self.workspace_id {
ws.id() == ws_id
} else {
false
}
};
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
{
// The workspace could've moved to a different output in the meantime.
(*mon.output() == self.output).then_some(ws_idx)
} else {
None
};
if let Some(ws_idx) = ws_idx {
layout.toggle_overview_to_workspace(ws_idx);
}
}
if let Some(window) = self.window.as_ref() {
layout.activate_window(window);
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_end(Some(false));
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(Some(false));
}
GestureState::InteractiveMove => {
layout.interactive_move_end(self.window.as_ref().unwrap());
}
};
state.niri.queue_redraw_all();
}
}
impl TouchGrab<State> for TouchOverviewGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
let timestamp = Duration::from_millis(u64::from(event.time));
let layout = &mut data.niri.layout;
// Check if we should become interactive move.
if matches!(self.gesture, GestureState::Recognizing) {
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
let passed = timestamp.saturating_sub(self.start_timestamp);
if INTERACTIVE_MOVE_THRESHOLD <= passed
&& layout.interactive_move_begin(
window.clone(),
&self.output,
self.start_pos_within_output,
)
{
self.gesture = GestureState::InteractiveMove;
}
}
}
// Check if we should become a spatial scroll.
if matches!(self.gesture, GestureState::Recognizing) {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
if c.x * c.x + c.y * c.y >= 16. * 16. {
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
self.gesture = GestureState::ViewOffset;
}
}
}
if matches!(self.gesture, GestureState::Recognizing) {
layout.workspace_switch_gesture_begin(&self.output, false);
self.gesture = GestureState::WorkspaceSwitch;
}
}
}
// Do nothing if still recognizing.
if matches!(self.gesture, GestureState::Recognizing) {
return;
}
let delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = match self.gesture {
GestureState::Recognizing => unreachable!(),
GestureState::ViewOffset => layout
.view_offset_gesture_update(-delta.x, timestamp, false)
.is_some(),
GestureState::WorkspaceSwitch => layout
.workspace_switch_gesture_update(-delta.y, timestamp, false)
.is_some(),
GestureState::InteractiveMove => {
let window = self.window.as_ref().unwrap();
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
data.niri.layout.interactive_move_update(
window,
delta,
output,
pos_within_output,
)
} else {
false
}
}
};
if ongoing {
data.niri.queue_redraw_all();
} else {
handle.unset_grab(self, data);
}
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+37 -195
View File
@@ -1,13 +1,9 @@
use std::io::ErrorKind;
use std::iter::Peekable;
use std::slice;
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
Response, Transform, Window,
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
};
use serde_json::json;
@@ -20,8 +16,6 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::PickWindow => Request::PickWindow,
Msg::PickColor => Request::PickColor,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
@@ -29,39 +23,27 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
},
Msg::Workspaces => Request::Workspaces,
Msg::Windows => Request::Windows,
Msg::Layers => Request::Layers,
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
Msg::OverviewState => Request::OverviewState,
};
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
let socket = Socket::connect().context("error connecting to the niri socket")?;
let result = socket.send(request);
let (reply, mut read_event) = socket
.send(request)
.context("error communicating with niri")?;
// For errors that can be caused by a version mismatch between the running niri instance and
// the niri msg CLI, we will try to fetch and compare the versions.
let check_compositor_version = match &result {
Err(err) => {
// Response JSON parsing errors.
matches!(
err.kind(),
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
)
let compositor_version = match reply {
Err(_) if !matches!(msg, Msg::Version) => {
// If we got an error, it might be that the CLI is a different version from the running
// niri instance. Request the running instance version to compare and print a message.
Socket::connect()
.and_then(|socket| socket.send(Request::Version))
.ok()
.map(|(reply, _read_event)| reply)
}
// Error returned from niri.
Ok(Err(_)) => true,
_ => false,
};
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
// Reconnect to support older niri versions with one request per connection.
Socket::connect()
.and_then(|mut socket| socket.send(Request::Version))
.ok()
} else {
None
_ => None,
};
// Default SIGPIPE so that our prints don't panic on stdout closing.
@@ -69,31 +51,32 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
let response = reply.map_err(|err_msg| {
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request.
// Don't add irrelevant context.
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request, or the
// original request had succeeded. Don't add irrelevant context.
}
}
let reply = result.context("error communicating with niri")?;
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
anyhow!(err_msg).context("niri returned an error")
})?;
match msg {
Msg::RequestError => {
@@ -185,69 +168,6 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!();
}
}
Msg::Layers => {
let Response::Layers(mut layers) = response else {
bail!("unexpected response: expected Layers, got {response:?}");
};
if json {
let layers = serde_json::to_string(&layers).context("error formatting response")?;
println!("{layers}");
return Ok(());
}
layers.sort_by(|a, b| {
Ord::cmp(&a.output, &b.output)
.then_with(|| Ord::cmp(&a.layer, &b.layer))
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
});
let mut iter = layers.iter().peekable();
let print = |surface: &niri_ipc::LayerSurface| {
println!(" Surface:");
println!(" Namespace: \"{}\"", &surface.namespace);
let interactivity = match surface.keyboard_interactivity {
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
};
println!(" Keyboard interactivity: {interactivity}");
};
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
output: &str,
layer| {
let mut empty = true;
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
empty = false;
println!();
print(surface);
}
if empty {
println!(" (empty)\n");
} else {
println!();
}
};
while let Some(surface) = iter.peek() {
let output = &surface.output;
println!("Output \"{output}\":");
print!(" Background layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Background);
print!(" Bottom layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
print!(" Top layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Top);
print!(" Overlay layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
}
}
Msg::FocusedOutput => {
let Response::FocusedOutput(output) = response else {
bail!("unexpected response: expected FocusedOutput, got {response:?}");
@@ -265,43 +185,6 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("No output is focused.");
}
}
Msg::PickWindow => {
let Response::PickedWindow(window) = response else {
bail!("unexpected response: expected PickedWindow, got {response:?}");
};
if json {
let window = serde_json::to_string(&window).context("error formatting response")?;
println!("{window}");
return Ok(());
}
if let Some(window) = window {
print_window(&window);
} else {
println!("No window selected.");
}
}
Msg::PickColor => {
let Response::PickedColor(color) = response else {
bail!("unexpected response: expected PickedColor, got {response:?}");
};
if json {
let color = serde_json::to_string(&color).context("error formatting response")?;
println!("{color}");
return Ok(());
}
if let Some(color) = color {
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
println!("Picked color: rgb({r}, {g}, {b})",);
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
} else {
println!("No color was picked.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
@@ -402,7 +285,6 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("Started reading events.");
}
let mut read_event = socket.read_events();
loop {
let event = read_event().context("error reading event from niri")?;
@@ -416,9 +298,6 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
println!("Workspace {id}: urgency changed to {urgent}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
@@ -444,40 +323,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::WindowUrgencyChanged { id, urgent } => {
println!("Window {id}: urgency changed to {urgent}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
Event::OverviewOpenedOrClosed { is_open: opened } => {
println!("Overview toggled: {opened}");
}
}
}
}
Msg::OverviewState => {
let Response::OverviewState(response) = response else {
bail!("unexpected response: expected Overview, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let Overview { is_open } = response;
if is_open {
println!("Overview is open.");
} else {
println!("Overview is closed.");
}
}
}
Ok(())
@@ -581,8 +435,7 @@ fn print_output(output: Output) -> anyhow::Result<()> {
fn print_window(window: &Window) {
let focused = if window.is_focused { " (focused)" } else { "" };
let urgent = if window.is_urgent { " (urgent)" } else { "" };
println!("Window ID {}:{focused}{urgent}", window.id);
println!("Window ID {}:{focused}", window.id);
if let Some(title) = &window.title {
println!(" Title: \"{title}\"");
@@ -596,17 +449,6 @@ fn print_window(window: &Window) {
println!(" App ID: (unset)");
}
println!(
" Is floating: {}",
if window.is_floating { "yes" } else { "no" }
);
if let Some(pid) = window.pid {
println!(" PID: {pid}");
} else {
println!(" PID: (unknown)");
}
if let Some(workspace_id) = window.workspace_id {
println!(" Workspace ID: {workspace_id}");
} else {
+117 -251
View File
@@ -1,6 +1,5 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::rc::Rc;
@@ -16,24 +15,17 @@ use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
};
use smithay::desktop::layer_map_for_output;
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::backend::IpcOutputMap;
use crate::input::pick_window_grab::PickWindowGrab;
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
use crate::utils::{version, with_toplevel_role};
use crate::utils::version;
use crate::window::Mapped;
// If an event stream client fails to read events fast enough that we accumulate more than this
@@ -41,10 +33,7 @@ use crate::window::Mapped;
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
pub struct IpcServer {
/// Path to the IPC socket.
///
/// This is `None` when creating `IpcServer` without a socket.
pub socket_path: Option<PathBuf>,
pub socket_path: PathBuf,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
@@ -71,38 +60,31 @@ struct EventStreamSender {
impl IpcServer {
pub fn start(
event_loop: &LoopHandle<'static, State>,
wayland_socket_name: Option<&OsStr>,
wayland_socket_name: &str,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("Ipc::start");
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
let wayland_socket_name = wayland_socket_name.to_string_lossy();
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
Ok(PostAction::Continue)
})
.unwrap();
Some(socket_path)
} else {
None
};
Ok(PostAction::Continue)
})
.unwrap();
Ok(Self {
socket_path,
@@ -137,9 +119,7 @@ impl IpcServer {
impl Drop for IpcServer {
fn drop(&mut self) {
if let Some(socket_path) = &self.socket_path {
let _ = unlink(socket_path);
}
let _ = unlink(&self.socket_path);
}
}
@@ -185,86 +165,76 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut read = BufReader::new(read);
let mut buf = String::new();
loop {
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
let mut buf = Vec::new();
let res = read.read_until(b'\n', &mut buf).await;
match res {
Ok(0) => return Ok(()),
Ok(_) => (),
// Normal client disconnection.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
Err(err) => {
return Err(err).context("error reading request");
}
}
// Read a single line to allow extensibility in the future to keep reading.
BufReader::new(read)
.read_line(&mut buf)
.await
.context("error reading request")?;
let request = serde_json::from_slice(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let request = serde_json::from_str(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
buf.clear();
serde_json::to_writer(&mut buf, &reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
return Ok(());
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
}
Ok(())
}
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
@@ -286,47 +256,6 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let windows = state.windows.windows.values().cloned().collect();
Response::Windows(windows)
}
Request::Layers => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let mut layers = Vec::new();
for output in state.niri.global_space.outputs() {
let name = output.name();
for surface in layer_map_for_output(output).layers() {
let layer = match surface.layer() {
Layer::Background => niri_ipc::Layer::Background,
Layer::Bottom => niri_ipc::Layer::Bottom,
Layer::Top => niri_ipc::Layer::Top,
Layer::Overlay => niri_ipc::Layer::Overlay,
};
let keyboard_interactivity =
match surface.cached_state().keyboard_interactivity {
KeyboardInteractivity::None => {
niri_ipc::LayerSurfaceKeyboardInteractivity::None
}
KeyboardInteractivity::Exclusive => {
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
}
KeyboardInteractivity::OnDemand => {
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
}
};
layers.push(niri_ipc::LayerSurface {
namespace: surface.namespace().to_owned(),
output: name.clone(),
layer,
keyboard_interactivity,
});
}
}
let _ = tx.send_blocking(layers);
});
let result = rx.recv().await;
let layers = result.map_err(|_| String::from("error getting layers info"))?;
Response::Layers(layers)
}
Request::KeyboardLayouts => {
let state = ctx.event_stream_state.borrow();
let layout = state.keyboard_layouts.keyboard_layouts.clone();
@@ -339,52 +268,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::PickWindow => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let pointer = state.niri.seat.get_pointer().unwrap();
let start_data = PointerGrabStartData {
focus: None,
button: 0,
location: pointer.current_location(),
};
let grab = PickWindowGrab::new(start_data);
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
// any.
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
state.niri.pick_window = Some(tx);
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
// Redraw to update the cursor.
state.niri.queue_redraw_all();
});
let result = rx.recv().await;
let id = result.map_err(|_| String::from("error getting picked window info"))?;
let window = id.and_then(|id| {
let state = ctx.event_stream_state.borrow();
state.windows.windows.get(&id.get()).cloned()
});
Response::PickedWindow(window)
}
Request::PickColor => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
state.handle_pick_color(tx);
});
let result = rx.recv().await;
let color = result.map_err(|_| String::from("error getting picked color"))?;
Response::PickedColor(color)
}
Request::Action(action) => {
let (tx, rx) = async_channel::bounded(1);
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
// Make sure some logic like workspace clean-up has a chance to run before doing
// actions.
state.niri.advance_animations();
state.do_action(action, false);
let _ = tx.send_blocking(());
});
@@ -440,11 +328,6 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Response::FocusedOutput(output)
}
Request::EventStream => Response::Handled,
Request::OverviewState => {
let state = ctx.event_stream_state.borrow();
let is_open = state.overview.is_open;
Response::OverviewState(Overview { is_open })
}
};
Ok(response)
@@ -478,15 +361,22 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result
}
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
pid: mapped.credentials().map(|c| c.pid),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
is_floating: mapped.is_floating(),
is_urgent: mapped.is_urgent(),
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
}
})
}
@@ -542,7 +432,6 @@ impl State {
pub fn ipc_refresh_layout(&mut self) {
self.ipc_refresh_workspaces();
self.ipc_refresh_windows();
self.ipc_refresh_overview();
}
fn ipc_refresh_workspaces(&mut self) {
@@ -590,12 +479,6 @@ impl State {
});
}
// Check if this workspace urgent state changed.
let urgent = ws.is_urgent();
if urgent != ipc_ws.is_urgent {
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
}
// Check if this workspace became focused.
let is_focused = Some(id) == focused_ws_id;
if is_focused && !ipc_ws.is_focused {
@@ -604,7 +487,7 @@ impl State {
}
// Check if this workspace became active.
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
if is_active && !ipc_ws.is_active {
events.push(Event::WorkspaceActivated { id, focused: false });
}
@@ -627,8 +510,7 @@ impl State {
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name().cloned(),
output: mon.map(|mon| mon.output_name().clone()),
is_urgent: ws.is_urgent(),
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
is_focused: Some(id) == focused_ws_id,
active_window_id: ws.active_window().map(|win| win.id().get()),
}
@@ -675,10 +557,17 @@ impl State {
};
let workspace_id = ws_id.map(|id| id.get());
let mut changed =
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
let mut changed = ipc_win.workspace_id != workspace_id;
let wl_surface = mapped.toplevel().wl_surface();
changed |= with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
changed |= with_toplevel_role(mapped.toplevel(), |role| {
ipc_win.title != role.title || ipc_win.app_id != role.app_id
});
@@ -691,11 +580,6 @@ impl State {
if mapped.is_focused() && !ipc_win.is_focused {
events.push(Event::WindowFocusChanged { id: Some(id) });
}
let urgent = mapped.is_urgent();
if urgent != ipc_win.is_urgent {
events.push(Event::WindowUrgencyChanged { id, urgent })
}
});
// Check for closed windows.
@@ -721,22 +605,4 @@ impl State {
server.send_event(event);
}
}
pub fn ipc_refresh_overview(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.overview;
let is_open = self.niri.layout.is_overview_open();
if state.is_open == is_open {
return;
}
let event = Event::OverviewOpenedOrClosed { is_open };
state.apply(event.clone());
server.send_event(event);
}
}
-218
View File
@@ -1,218 +0,0 @@
use niri_config::layer_rule::LayerRule;
use niri_config::Config;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,
/// Up-to-date rules.
rules: ResolvedLayerRules,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: SolidColorBuffer,
/// The shadow around the surface.
shadow: Shadow,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
/// Scale of the output the layer surface is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
clock: Clock,
}
niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
}
}
impl MappedLayer {
pub fn new(
surface: LayerSurface,
rules: ResolvedLayerRules,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
config: &Config,
) -> Self {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = rules.shadow.resolve_against(shadow_config);
Self {
surface,
rules,
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
view_size,
scale,
shadow: Shadow::new(shadow_config),
clock,
}
}
pub fn update_config(&mut self, config: &Config) {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
self.shadow.update_config(shadow_config);
}
pub fn update_shaders(&mut self) {
self.shadow.update_shaders();
}
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
self.view_size = view_size;
self.scale = scale;
}
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
// Round to physical pixels.
let size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
self.block_out_buffer.resize(size);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
// FIXME: is_active based on keyboard focus?
self.shadow
.update_render_elements(size, true, radius, self.scale, 1.);
}
pub fn are_animations_ongoing(&self) -> bool {
self.rules.baba_is_float
}
pub fn surface(&self) -> &LayerSurface {
&self.surface
}
pub fn rules(&self) -> &ResolvedLayerRules {
&self.rules
}
/// Recomputes the resolved layer rules and returns whether they changed.
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
if new_rules == self.rules {
return false;
}
self.rules = new_rules;
true
}
pub fn place_within_backdrop(&self) -> bool {
if !self.rules.place_within_backdrop {
return false;
}
if self.surface.layer() != Layer::Background {
return false;
}
let state = self.surface.cached_state();
if state.exclusive_zone != ExclusiveZone::DontCare {
return false;
}
true
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if !self.rules.baba_is_float {
return Point::from((0., 0.));
}
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
// FIXME: take geometry-corner-radius into account.
let elem = SolidColorRenderElement::from_buffer(
&self.block_out_buffer,
location,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = location;
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;
rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
));
}
rv.normal = render_elements_from_surface_tree(
renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
}
let location = location.to_physical_precise_round(scale).to_logical(scale);
rv.normal
.extend(self.shadow.render(renderer, location).map(Into::into));
rv
}
}
-106
View File
@@ -1,106 +0,0 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use smithay::desktop::LayerSurface;
pub mod mapped;
pub use mapped::MappedLayer;
/// Rules fully resolved for a layer-shell surface.
#[derive(Debug, PartialEq)]
pub struct ResolvedLayerRules {
/// Extra opacity to draw this layer surface with.
pub opacity: Option<f32>,
/// Whether to block out this layer surface from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
/// Shadow overrides.
pub shadow: ShadowRule,
/// Corner radius to assume this layer surface has.
pub geometry_corner_radius: Option<CornerRadius>,
/// Whether to place this layer surface within the overview backdrop.
pub place_within_backdrop: bool,
/// Whether to bob this window up and down.
pub baba_is_float: bool,
}
impl ResolvedLayerRules {
pub const fn empty() -> Self {
Self {
opacity: None,
block_out_from: None,
shadow: ShadowRule {
off: false,
on: false,
offset: None,
softness: None,
spread: None,
draw_behind_window: None,
color: None,
inactive_color: None,
},
geometry_corner_radius: None,
place_within_backdrop: false,
baba_is_float: false,
}
}
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
let _span = tracy_client::span!("ResolvedLayerRules::compute");
let mut resolved = ResolvedLayerRules::empty();
for rule in rules {
let matches = |m: &Match| {
if let Some(at_startup) = m.at_startup {
if at_startup != is_at_startup {
return false;
}
}
surface_matches(surface, m)
};
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}
if rule.excludes.iter().any(matches) {
continue;
}
if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
if let Some(x) = rule.geometry_corner_radius {
resolved.geometry_corner_radius = Some(x);
}
if let Some(x) = rule.place_within_backdrop {
resolved.place_within_backdrop = x;
}
if let Some(x) = rule.baba_is_float {
resolved.baba_is_float = x;
}
resolved.shadow.merge_with(&rule.shadow);
}
resolved
}
}
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
if let Some(namespace_re) = &m.namespace {
if !namespace_re.0.is_match(surface.namespace()) {
return false;
}
}
true
}
+5 -3
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -137,15 +138,16 @@ impl ClosingWindow {
})
}
pub fn advance_animations(&mut self) {
pub fn advance_animations(&mut self, current_time: Duration) {
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let anim = anim.restarted(0., 1., 0.);
let mut anim = anim.restarted(0., 1., 0.);
anim.set_current_time(current_time);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(_anim) => (),
AnimationState::Animating(anim) => anim.set_current_time(current_time),
}
}
File diff suppressed because it is too large Load Diff
+20 -26
View File
@@ -1,8 +1,8 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
use smithay::backend::renderer::element::{Element as _, Kind};
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::niri_render_elements;
@@ -53,24 +53,19 @@ impl FocusRing {
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
is_urgent: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_urgent {
self.config.urgent_color
} else if is_active {
let color = if is_active {
self.config.active_color
} else {
self.config.inactive_color
@@ -82,9 +77,7 @@ impl FocusRing {
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
let gradient = if is_urgent {
self.config.urgent_gradient
} else if is_active {
let gradient = if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
@@ -93,9 +86,15 @@ impl FocusRing {
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
@@ -179,16 +178,15 @@ impl FocusRing {
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::new(full_rect.loc - loc, full_rect.size),
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
} else {
@@ -198,16 +196,18 @@ impl FocusRing {
self.borders[0].update(
self.sizes[0],
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
Rectangle::from_loc_and_size(
gradient_area.loc - self.locations[0],
gradient_area.size,
),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
@@ -238,9 +238,7 @@ impl FocusRing {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
let alpha = border.alpha();
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
.into()
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
};
rv.push(elem);
};
@@ -267,8 +265,4 @@ impl FocusRing {
pub fn is_off(&self) -> bool {
self.config.off
}
pub fn config(&self) -> &niri_config::FocusRing {
&self.config
}
}
-65
View File
@@ -1,65 +0,0 @@
use niri_config::{CornerRadius, FloatOrInt};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct InsertHintElement {
inner: FocusRing,
}
pub type InsertHintRenderElement = FocusRingRenderElement;
impl InsertHintElement {
pub fn new(config: niri_config::InsertHint) -> Self {
Self {
inner: FocusRing::new(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_gradient: config.gradient,
}),
}
}
pub fn update_config(&mut self, config: niri_config::InsertHint) {
self.inner.update_config(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_gradient: config.gradient,
});
}
pub fn update_shaders(&mut self) {
self.inner.update_shaders();
}
pub fn update_render_elements(
&mut self,
size: Size<f64, Logical>,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
self.inner
.update_render_elements(size, true, false, false, view_rect, radius, scale, 1.);
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
self.inner.render(renderer, location)
}
}
+3080 -2558
View File
File diff suppressed because it is too large Load Diff
+522 -1277
View File
File diff suppressed because it is too large Load Diff
+43 -32
View File
@@ -1,31 +1,34 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
buffer: OffscreenBuffer,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
@@ -35,11 +38,12 @@ impl OpenAnimation {
Self {
anim,
random_seed: fastrand::f32(),
buffer: OffscreenBuffer::default(),
}
}
pub fn advance_animations(&mut self) {}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn is_done(&self) -> bool {
self.anim.is_done()
@@ -54,24 +58,24 @@ impl OpenAnimation {
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
) -> anyhow::Result<OpeningWindowRenderElement> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (elem, _sync_point, mut data) = self
.buffer
.render(renderer, scale, elements)
.context("error rendering to offscreen buffer")?;
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
// can assume that below.
let offset = elem.offset();
let texture = elem.texture();
let texture_size = elem.logical_size();
let mut area = Rectangle::new(location + offset, texture_size);
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
let mut target_size = area.size.upscale(1.5);
@@ -98,12 +102,12 @@ impl OpenAnimation {
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
let elem = ShaderRenderElement::new(
return Ok(ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
alpha,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
@@ -115,29 +119,36 @@ impl OpenAnimation {
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc);
// We're drawing the shader, not the offscreen itself.
data.id = elem.id().clone();
return Ok((elem.into(), data));
.with_location(area.loc)
.into());
}
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
center.to_physical_precise_round(scale),
(center - offset).to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
(location + offset).to_physical_precise_round(scale),
Relocate::Relative,
);
Ok((elem.into(), data))
Ok(elem.into())
}
}
File diff suppressed because it is too large Load Diff
-184
View File
@@ -1,184 +0,0 @@
use std::iter::zip;
use niri_config::CornerRadius;
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
#[derive(Debug)]
pub struct Shadow {
shader_rects: Vec<Rectangle<f64, Logical>>,
shaders: Vec<ShadowRenderElement>,
config: niri_config::Shadow,
}
impl Shadow {
pub fn new(config: niri_config::Shadow) -> Self {
Self {
shader_rects: Vec::new(),
shaders: Vec::new(),
config,
}
}
pub fn update_config(&mut self, config: niri_config::Shadow) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size is rounded to physical pixels before being passed to this function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
let width = self.config.softness.0;
// Like in CSS box-shadow.
let sigma = width / 2.;
// Adjust width to draw all necessary pixels.
let width = ceil(sigma * 3.);
let offset = self.config.offset;
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
let spread = self.config.spread.0;
let spread = ceil(spread.abs()).copysign(spread);
let offset = offset - Point::from((spread, spread));
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
let box_size = if spread >= 0. {
win_size + Size::from((spread, spread)).upscale(2.)
} else {
// This is a saturating sub.
win_size - Size::from((-spread, -spread)).upscale(2.)
};
let radius = win_radius.expanded_by(spread as f32);
let shader_size = box_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.color
} else {
// Default to slightly more transparent.
self.config
.inactive_color
.unwrap_or(self.config.color * 0.75)
};
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
// This is actually offset relative to shader_geo, this is handled below.
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
if !self.config.draw_behind_window {
let top_left = ceil(f64::from(win_radius.top_left));
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
let bottom_left = f64::min(
win_size.h - top_left,
ceil(f64::from(win_radius.bottom_left)),
);
let bottom_right = f64::min(
win_size.h - top_right,
f64::min(
win_size.w - bottom_left,
ceil(f64::from(win_radius.bottom_right)),
),
);
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
let top_right = Rectangle::new(
Point::from((win_size.w - top_right, 0.)),
Size::from((top_right, top_right)),
);
let bottom_right = Rectangle::new(
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
Size::from((bottom_right, bottom_right)),
);
let bottom_left = Rectangle::new(
Point::from((0., win_size.h - bottom_left)),
Size::from((bottom_left, bottom_left)),
);
let mut background =
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
for rect in &mut background {
rect.loc -= offset;
}
self.shader_rects = shader_geo.subtract_rects(background);
self.shaders
.resize_with(self.shader_rects.len(), Default::default);
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
shader.update(
rect.size,
Rectangle::new(rect.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
win_radius,
alpha,
);
rect.loc += offset;
}
} else {
self.shader_rects.resize_with(1, Default::default);
self.shader_rects[0] = shader_geo;
self.shaders.resize_with(1, Default::default);
self.shaders[0].update(
shader_geo.size,
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::zero(),
Default::default(),
alpha,
);
self.shader_rects[0].loc += offset;
}
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
if !self.config.on {
return None.into_iter().flatten();
}
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
if !has_shadow_shader {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_rects)
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
Some(rv).into_iter().flatten()
}
}
-412
View File
@@ -1,412 +0,0 @@
use std::iter::zip;
use std::mem;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::tile::Tile;
use super::LayoutElement;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
};
#[derive(Debug)]
pub struct TabIndicator {
shader_locs: Vec<Point<f64, Logical>>,
shaders: Vec<BorderRenderElement>,
open_anim: Option<Animation>,
config: niri_config::TabIndicator,
}
#[derive(Debug)]
pub struct TabInfo {
/// Gradient for the tab indicator.
pub gradient: Gradient,
/// Tab geometry in the same coordinate system as the area.
pub geometry: Rectangle<f64, Logical>,
}
niri_render_elements! {
TabIndicatorRenderElement => {
Gradient = BorderRenderElement,
}
}
impl TabIndicator {
pub fn new(config: niri_config::TabIndicator) -> Self {
Self {
shader_locs: Vec::new(),
shaders: Vec::new(),
open_anim: None,
config,
}
}
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn advance_animations(&mut self) {
if let Some(anim) = &mut self.open_anim {
if anim.is_done() {
self.open_anim = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_anim.is_some()
}
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
}
fn tab_rects(
&self,
area: Rectangle<f64, Logical>,
count: usize,
scale: f64,
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let round = |logical: f64| round_logical_in_physical(scale, logical);
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
let width = round_max1(self.config.width.0);
let gap = self.config.gap.0;
let gap = round_max1(gap.abs()).copysign(gap);
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position;
let side = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
};
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
let min_length = round(side * total_prop.clamp(0., 2.));
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
// growing and shrinking over the duration of the animation.
let pixel = 1. / scale;
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
let length = f64::max(min_length, shortest_length);
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
let px_per_tab = px_per_tab * progress;
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
match position {
TabIndicatorPosition::Left => (),
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
TabIndicatorPosition::Bottom => {
shader_loc.x = shader_loc.y;
shader_loc.y = area.size.h + gap;
}
}
shader_loc += area.loc;
(0..count).map(move |_| {
let mut px_per_tab = px_per_tab;
if ones_left > 0 {
ones_left -= 1;
px_per_tab += pixel;
}
let loc = shader_loc;
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
shader_loc.y += px_per_tab + gaps_between
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
shader_loc.x += px_per_tab + gaps_between
}
}
let size = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
Size::from((width, px_per_tab))
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
Size::from((px_per_tab, width))
}
};
Rectangle::new(loc, size)
})
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
enabled: bool,
// Geometry of the tabs area.
area: Rectangle<f64, Logical>,
// View rect relative to the tabs area.
area_view_rect: Rectangle<f64, Logical>,
// Tab count, should match the tabs iterator length.
tab_count: usize,
tabs: impl Iterator<Item = TabInfo>,
is_active: bool,
scale: f64,
) {
if !enabled || self.config.off {
self.shader_locs.clear();
self.shaders.clear();
return;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
self.shader_locs.clear();
self.shaders.clear();
return;
}
self.shaders.resize_with(count, Default::default);
self.shader_locs.resize_with(count, Default::default);
let position = self.config.position;
let radius = self.config.corner_radius.0 as f32;
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
let mut tabs_left = tab_count;
let rects = self.tab_rects(area, count, scale);
for ((shader, loc), (tab, rect)) in zip(
zip(&mut self.shaders, &mut self.shader_locs),
zip(tabs, rects),
) {
*loc = rect.loc;
let mut gradient_area = match tab.gradient.relative_to {
GradientRelativeTo::Window => tab.geometry,
GradientRelativeTo::WorkspaceView => area_view_rect,
};
gradient_area.loc -= *loc;
let mut color_from = tab.gradient.from;
let mut color_to = tab.gradient.to;
if !is_active {
color_from *= 0.5;
color_to *= 0.5;
}
let radius = if shared_rounded_corners && tab_count > 1 {
if tabs_left == tab_count {
// First tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: radius,
top_right: radius,
bottom_right: 0.,
bottom_left: 0.,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: radius,
top_right: 0.,
bottom_right: 0.,
bottom_left: radius,
},
}
} else if tabs_left == 1 {
// Last tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: 0.,
top_right: 0.,
bottom_right: radius,
bottom_left: radius,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: 0.,
top_right: radius,
bottom_right: radius,
bottom_left: 0.,
},
}
} else {
// Tab in the middle.
CornerRadius::default()
}
} else {
// Separate tabs, or the only tab.
CornerRadius::from(radius)
};
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
tabs_left -= 1;
shader.update(
rect.size,
gradient_area,
tab.gradient.in_,
color_from,
color_to,
((tab.gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_size(rect.size),
0.,
radius,
scale as f32,
1.,
);
}
}
pub fn hit(
&self,
area: Rectangle<f64, Logical>,
tab_count: usize,
scale: f64,
point: Point<f64, Logical>,
) -> Option<usize> {
if self.config.off {
return None;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
return None;
}
self.tab_rects(area, count, scale)
.enumerate()
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
pos: Point<f64, Logical>,
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
let has_border_shader = BorderRenderElement::has_shader(renderer);
if !has_border_shader {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_locs)
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
.map(TabIndicatorRenderElement::from);
Some(rv).into_iter().flatten()
}
/// Extra size occupied by the tab indicator.
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
if self.config.off
|| !self.config.place_within_column
|| (self.config.hide_when_single_tab && tab_count == 1)
{
return Size::from((0., 0.));
}
let round = |logical: f64| round_logical_in_physical(scale, logical);
let width = round(self.config.width.0);
let gap = round(self.config.gap.0);
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
// that it peeks from the other side of the window".
let size = f64::max(0., width + gap);
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
}
}
/// Offset of the tabbed content due to space occupied by the tab indicator.
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
self.extra_size(tab_count, scale).to_point()
}
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
}
}
pub fn config(&self) -> niri_config::TabIndicator {
self.config
}
}
impl TabInfo {
pub fn from_tile<W: LayoutElement>(
tile: &Tile<W>,
position: Point<f64, Logical>,
is_active: bool,
is_urgent: bool,
config: &niri_config::TabIndicator,
) -> Self {
let rules = tile.window().rules();
let rule = rules.tab_indicator;
let gradient_from_rule = || {
let (color, gradient) = if is_urgent {
(rule.urgent_color, rule.urgent_gradient)
} else if is_active {
(rule.active_color, rule.active_gradient)
} else {
(rule.inactive_color, rule.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_config = || {
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_border = || {
// Come up with tab indicator gradient matching the focus ring or the border, whichever
// one is enabled.
let focus_ring_config = tile.focus_ring().config();
let border_config = tile.border().config();
let config = if focus_ring_config.off {
border_config
} else {
focus_ring_config
};
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
gradient.unwrap_or_else(|| Gradient::from(color))
};
let gradient = gradient_from_rule()
.or_else(gradient_from_config)
.unwrap_or_else(gradient_from_border);
let geometry = Rectangle::new(position, tile.animated_tile_size());
TabInfo { gradient, geometry }
}
}
-3666
View File
File diff suppressed because it is too large Load Diff
+127 -394
View File
@@ -1,32 +1,29 @@
use core::f64;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::shadow::Shadow;
use super::{
HitType, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
SizeFrac, RESIZE_ANIMATION_THRESHOLD,
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::{Animation, Clock};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::utils::transaction::Transaction;
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -38,11 +35,11 @@ pub struct Tile<W: LayoutElement> {
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,
/// The shadow around the window.
shadow: Shadow,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
@@ -52,27 +49,8 @@ pub struct Tile<W: LayoutElement> {
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
/// Whether the tile should float upon unfullscreening.
pub(super) unfullscreen_to_floating: bool,
/// The size that the window should assume when going floating.
///
/// This is generally the last size the window had when it was floating. It can be unknown if
/// the window starts out in the tiling layout or fullscreen.
pub(super) floating_window_size: Option<Size<i32, Logical>>,
/// The position that the tile should assume when going floating, relative to the floating
/// space working area.
///
/// This is generally the last position the tile had when it was floating. It can be unknown if
/// the window starts out in the tiling layout.
pub(super) floating_pos: Option<Point<f64, SizeFrac>>,
/// Currently selected preset width index when this tile is floating.
pub(super) floating_preset_width_idx: Option<usize>,
/// Currently selected preset height index when this tile is floating.
pub(super) floating_preset_height_idx: Option<usize>,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<f64, Logical>,
/// The animation upon opening a window.
open_animation: Option<OpenAnimation>,
@@ -86,9 +64,6 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// The animation of the tile's opacity.
pub(super) alpha_animation: Option<AlphaAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
@@ -98,17 +73,9 @@ pub struct Tile<W: LayoutElement> {
/// Extra damage for clipped surface corner radius changes.
rounded_corner_damage: RoundedCornerDamage,
/// The view size for the tile's workspace.
///
/// Used as the fullscreen target size.
view_size: Size<f64, Logical>,
/// Scale of the output the tile is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
pub(super) options: Rc<Options>,
}
@@ -121,9 +88,7 @@ niri_render_elements! {
Opening = OpeningWindowRenderElement,
Resize = ResizeRenderElement,
Border = BorderRenderElement,
Shadow = ShadowRenderElement,
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
}
}
@@ -136,7 +101,6 @@ struct ResizeAnimation {
anim: Animation,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
offscreen: OffscreenBuffer,
}
#[derive(Debug)]
@@ -145,74 +109,32 @@ struct MoveAnimation {
from: f64,
}
#[derive(Debug)]
pub(super) struct AlphaAnimation {
pub(super) anim: Animation,
/// Whether the animation should persist after it's done.
///
/// This is used by things like interactive move which need to animate alpha to
/// semitransparent, then hold it at semitransparent for a while, until the operation
/// completes.
pub(super) hold_after_done: bool,
offscreen: OffscreenBuffer,
}
impl<W: LayoutElement> Tile<W> {
pub fn new(
window: W,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
options: Rc<Options>,
) -> Self {
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
let rules = window.rules();
let border_config = rules.border.resolve_against(options.border);
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
let shadow_config = rules.shadow.resolve_against(options.shadow);
let is_fullscreen = window.is_fullscreen();
Self {
window,
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
shadow: Shadow::new(shadow_config),
is_fullscreen,
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
unfullscreen_to_floating: false,
floating_window_size: None,
floating_pos: None,
floating_preset_width_idx: None,
floating_preset_height_idx: None,
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,
resize_animation: None,
move_x_animation: None,
move_y_animation: None,
alpha_animation: None,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
view_size,
scale,
clock,
options,
}
}
pub fn update_config(
&mut self,
view_size: Size<f64, Logical>,
scale: f64,
options: Rc<Options>,
) {
// If preset widths or heights changed, clear our stored preset index.
if self.options.preset_column_widths != options.preset_column_widths {
self.floating_preset_width_idx = None;
}
if self.options.preset_window_heights != options.preset_window_heights {
self.floating_preset_height_idx = None;
}
self.view_size = view_size;
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
self.scale = scale;
self.options = options;
@@ -225,24 +147,21 @@ impl<W: LayoutElement> Tile<W> {
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
self.fullscreen_backdrop.resize(view_size);
}
pub fn update_shaders(&mut self) {
self.border.update_shaders();
self.focus_ring.update_shaders();
self.shadow.update_shaders();
}
pub fn update_window(&mut self) {
self.is_fullscreen = self.window.is_fullscreen();
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0., 0.)) {
self.is_fullscreen = self.window.is_fullscreen();
}
if let Some(animate_from) = self.window.take_animation_snapshot() {
let (size_from, offscreen) = if let Some(resize) = self.resize_animation.take() {
let size_from = if let Some(resize) = self.resize_animation.take() {
// Compute like in animated_window_size(), but using the snapshot geometry (since
// the current one is already overwritten).
let mut size = animate_from.size;
@@ -253,27 +172,19 @@ impl<W: LayoutElement> Tile<W> {
size.w = size_from.w + (size.w - size_from.w) * val;
size.h = size_from.h + (size.h - size_from.h) * val;
// Also try to reuse the existing offscreen buffer if we have one.
(size, resize.offscreen)
size
} else {
(animate_from.size, OffscreenBuffer::default())
animate_from.size
};
let change = self.window.size().to_f64().to_point() - size_from.to_point();
let change = f64::max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(
self.clock.clone(),
0.,
1.,
0.,
self.options.animations.window_resize.anim,
);
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
snapshot: animate_from,
offscreen,
});
} else {
self.resize_animation = None;
@@ -288,9 +199,6 @@ impl<W: LayoutElement> Tile<W> {
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
@@ -300,53 +208,43 @@ impl<W: LayoutElement> Tile<W> {
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self) {
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(open) = &mut self.open_animation {
open.advance_animations(current_time);
if open.is_done() {
self.open_animation = None;
}
}
if let Some(resize) = &mut self.resize_animation {
resize.anim.set_current_time(current_time);
if resize.anim.is_done() {
self.resize_animation = None;
}
}
if let Some(move_) = &mut self.move_x_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_x_animation = None;
}
}
if let Some(move_) = &mut self.move_y_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_y_animation = None;
}
}
if let Some(alpha) = &mut self.alpha_animation {
if !alpha.hold_after_done && alpha.anim.is_done() {
self.alpha_animation = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.are_transitions_ongoing() || self.window.rules().baba_is_float == Some(true)
}
pub fn are_transitions_ongoing(&self) -> bool {
self.open_animation.is_some()
|| self.resize_animation.is_some()
|| self.move_x_animation.is_some()
|| self.move_y_animation.is_some()
|| self
.alpha_animation
.as_ref()
.is_some_and(|alpha| !alpha.anim.is_done())
}
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
let rules = self.window.rules();
let draw_border_with_background = rules
@@ -366,29 +264,12 @@ impl<W: LayoutElement> Tile<W> {
self.animated_window_size(),
is_active,
!draw_border_with_background,
self.window.is_urgent(),
Rectangle::new(
Rectangle::from_loc_and_size(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
),
radius,
self.scale,
1.,
);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
};
self.shadow.update_render_elements(
self.animated_tile_size(),
is_active,
radius,
self.scale,
1.,
);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@@ -396,16 +277,21 @@ impl<W: LayoutElement> Tile<W> {
} else {
draw_border_with_background
};
let radius = radius.expanded_by(self.focus_ring.width() as f32);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
}
.expanded_by(self.focus_ring.width() as f32);
self.focus_ring.update_render_elements(
self.animated_tile_size(),
is_active,
!draw_focus_ring_with_background,
self.window.is_urgent(),
view_rect,
radius,
self.scale,
1.,
);
}
@@ -430,7 +316,6 @@ impl<W: LayoutElement> Tile<W> {
pub fn start_open_animation(&mut self) {
self.open_animation = Some(OpenAnimation::new(Animation::new(
self.clock.clone(),
0.,
1.,
0.,
@@ -458,7 +343,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_x_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
self.move_x_animation = Some(MoveAnimation {
anim,
@@ -477,7 +362,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_y_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
self.move_y_animation = Some(MoveAnimation {
anim,
@@ -490,39 +375,6 @@ impl<W: LayoutElement> Tile<W> {
self.move_y_animation = None;
}
pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) {
let from = from.clamp(0., 1.);
let to = to.clamp(0., 1.);
let (current, offscreen) = if let Some(alpha) = self.alpha_animation.take() {
(alpha.anim.clamped_value(), alpha.offscreen)
} else {
(from, OffscreenBuffer::default())
};
self.alpha_animation = Some(AlphaAnimation {
anim: Animation::new(self.clock.clone(), current, to, 0., config),
hold_after_done: false,
offscreen,
});
}
pub fn ensure_alpha_animates_to_1(&mut self) {
if let Some(alpha) = &self.alpha_animation {
if alpha.anim.to() != 1. {
// Cancel animation instead of starting a new one because the user likely wants to
// see the tile right away.
self.alpha_animation = None;
}
}
}
pub fn hold_alpha_animation_after_done(&mut self) {
if let Some(alpha) = &mut self.alpha_animation {
alpha.hold_after_done = true;
}
}
pub fn window(&self) -> &W {
&self.window
}
@@ -531,12 +383,16 @@ impl<W: LayoutElement> Tile<W> {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
pub fn is_fullscreen(&self) -> bool {
self.is_fullscreen
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
pub fn effective_border_width(&self) -> Option<f64> {
fn effective_border_width(&self) -> Option<f64> {
if self.is_fullscreen {
return None;
}
@@ -555,7 +411,7 @@ impl<W: LayoutElement> Tile<W> {
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window_size();
let target_size = self.view_size;
let target_size = self.fullscreen_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
@@ -585,27 +441,8 @@ impl<W: LayoutElement> Tile<W> {
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
let mut size = self.window_expected_or_current_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
return size;
}
@@ -625,16 +462,7 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
let size = self.window.expected_size();
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
size
}
pub fn animated_window_size(&self) -> Size<f64, Logical> {
fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
if let Some(resize) = &self.resize_animation {
@@ -651,14 +479,14 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
return size;
}
@@ -677,32 +505,16 @@ impl<W: LayoutElement> Tile<W> {
loc
}
fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
point -= self.window_loc().to_f64();
self.window.is_in_input_region(point)
}
fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_size(self.tile_size());
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
activation_region.contains(point)
}
pub fn hit(&self, point: Point<f64, Logical>) -> Option<HitType> {
let offset = self.bob_offset();
let point = point - offset;
if self.is_in_input_region(point) {
let win_pos = self.buf_loc() + offset;
Some(HitType::Input { win_pos })
} else if self.is_in_activation_region(point) {
Some(HitType::Activate {
is_tab_indicator: false,
})
} else {
None
}
}
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
@@ -720,7 +532,7 @@ impl<W: LayoutElement> Tile<W> {
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window
.request_size(size.to_i32_floor(), false, animate, transaction);
.request_size(size.to_i32_floor(), animate, transaction);
}
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
@@ -755,18 +567,16 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn request_fullscreen(&mut self, animate: bool, transaction: Option<Transaction>) {
self.window
.request_size(self.view_size.to_i32_round(), true, animate, transaction);
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size.to_i32_round());
}
pub fn min_size_nonfullscreen(&self) -> Size<f64, Logical> {
pub fn min_size(&self) -> Size<f64, Logical> {
let mut size = self.window.min_size().to_f64();
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
if let Some(width) = self.effective_border_width() {
size.w = f64::max(1., size.w);
size.h = f64::max(1., size.h);
@@ -777,13 +587,10 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn max_size_nonfullscreen(&self) -> Size<f64, Logical> {
pub fn max_size(&self) -> Size<f64, Logical> {
let mut size = self.window.max_size().to_f64();
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
if let Some(width) = self.effective_border_width() {
if size.w > 0. {
size.w += width * 2.;
}
@@ -795,16 +602,6 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if self.window.rules().baba_is_float != Some(true) {
return Point::from((0., 0.));
}
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
@@ -816,38 +613,27 @@ impl<W: LayoutElement> Tile<W> {
.unwrap_or_else(|| !self.window.has_ssd())
}
fn render_inner<'a, R: NiriRenderer + 'a>(
&'a self,
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
) -> impl Iterator<Item = TileRenderElement<R>> {
let _span = tracy_client::span!("Tile::render_inner");
let scale = Scale::from(self.scale);
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
let alpha = if self.is_fullscreen {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
// This is here rather than in render_offset() because render_offset() is currently assumed
// by the code to be temporary. So, for example, interactive move will try to "grab" the
// tile at its current render offset and reset the render offset to zero by cancelling the
// tile move animations. On the other hand, bob_offset() is not resettable, so adding it in
// render_offset() would cause obvious animation glitches.
//
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
let window_loc = self.window_loc();
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::new(window_render_loc, animated_window_size);
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
let rules = self.window.rules();
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
@@ -861,7 +647,7 @@ impl<W: LayoutElement> Tile<W> {
if let Some(resize) = &self.resize_animation {
resize_popups = Some(
self.window
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
.render_popups(renderer, window_render_loc, scale, alpha, target)
.into_iter()
.map(Into::into),
);
@@ -878,11 +664,15 @@ impl<W: LayoutElement> Tile<W> {
target,
);
let current = resize
.offscreen
.render(gles_renderer, scale, &window_elements)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
let current = render_to_encompassing_texture(
gles_renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&window_elements,
)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
@@ -895,13 +685,7 @@ impl<W: LayoutElement> Tile<W> {
clip_to_geometry
};
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
// The offset and size are computed in physical pixels and converted to
// logical with the same `scale`, so converting them back with rounding
// inside the geometry() call gives us the same physical result back.
let texture_current_geo = elem_current.geometry(scale);
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
let elem = ResizeRenderElement::new(
area,
scale,
@@ -913,15 +697,12 @@ impl<W: LayoutElement> Tile<W> {
resize.anim.clamped_value().clamp(0., 1.) as f32,
radius,
clip_to_geometry,
win_alpha,
alpha,
);
// We're drawing the resize shader, not the offscreen directly.
data.id = elem.id().clone();
// This is not a problem for split popups as the code will look for them by
// original id when it doesn't find them on the offscreen.
self.window.set_offscreen_data(Some(data));
// FIXME: with split popups, this will use the resize element ID for
// popups, but we want the real IDs.
self.window
.set_offscreen_element_id(Some(elem.id().clone()));
resize_shader = Some(elem.into());
}
}
@@ -933,11 +714,12 @@ impl<W: LayoutElement> Tile<W> {
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc,
win_alpha,
alpha,
Kind::Unspecified,
)
.into(),
);
self.window.set_offscreen_element_id(None);
}
}
@@ -948,9 +730,9 @@ impl<W: LayoutElement> Tile<W> {
if resize_shader.is_none() && resize_fallback.is_none() {
let window = self
.window
.render(renderer, window_render_loc, scale, win_alpha, target);
.render(renderer, window_render_loc, scale, alpha, target);
let geo = Rectangle::new(window_render_loc, window_size);
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
@@ -992,16 +774,15 @@ impl<W: LayoutElement> Tile<W> {
if radius != CornerRadius::default() && has_border_shader {
return BorderRenderElement::new(
geo.size,
Rectangle::from_size(geo.size),
Rectangle::from_loc_and_size((0., 0.), geo.size),
GradientInterpolation::default(),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_size(geo.size),
Rectangle::from_loc_and_size((0., 0.), geo.size),
0.,
radius,
scale.x as f32,
1.,
)
.with_location(geo.loc)
.into();
@@ -1043,98 +824,81 @@ impl<W: LayoutElement> Tile<W> {
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
let rv = rv.chain(elem.into_iter().flatten());
rv.chain(self.shadow.render(renderer, location).map(Into::into))
rv.chain(elem.into_iter().flatten())
}
pub fn render<'a, R: NiriRenderer + 'a>(
&'a self,
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
) -> impl Iterator<Item = TileRenderElement<R>> {
let _span = tracy_client::span!("Tile::render");
let scale = Scale::from(self.scale);
let tile_alpha = self
.alpha_animation
.as_ref()
.map_or(1., |alpha| alpha.anim.clamped_value()) as f32;
let mut open_anim_elem = None;
let mut alpha_anim_elem = None;
let mut window_elems = None;
self.window().set_offscreen_data(None);
if let Some(open) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements =
self.render_inner(renderer, Point::from((0., 0.)), scale, focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match open.render(
renderer,
&elements,
self.animated_tile_size(),
location,
scale,
tile_alpha,
) {
Ok((elem, data)) => {
self.window().set_offscreen_data(Some(data));
match open.render(renderer, &elements, self.tile_size(), location, scale) {
Ok(elem) => {
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
open_anim_elem = Some(elem.into());
}
Err(err) => {
warn!("error rendering window opening animation: {err:?}");
}
}
} else if let Some(alpha) = &self.alpha_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match alpha.offscreen.render(renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
let offset = elem.offset();
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
self.window().set_offscreen_data(Some(data));
alpha_anim_elem = Some(elem.into());
}
Err(err) => {
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
}
}
}
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
if open_anim_elem.is_none() {
self.window().set_offscreen_element_id(None);
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
}
open_anim_elem
.into_iter()
.chain(alpha_anim_elem)
.chain(window_elems.into_iter().flatten())
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer));
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
}
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
let contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
);
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let blocked_out_contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Screencast,
);
@@ -1152,35 +916,4 @@ impl<W: LayoutElement> Tile<W> {
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
self.unmap_snapshot.take()
}
pub fn border(&self) -> &FocusRing {
&self.border
}
pub fn focus_ring(&self) -> &FocusRing {
&self.focus_ring
}
pub fn options(&self) -> &Rc<Options> {
&self.options
}
#[cfg(test)]
pub fn view_size(&self) -> Size<f64, Logical> {
self.view_size
}
#[cfg(test)]
pub fn verify_invariants(&self) {
use approx::assert_abs_diff_eq;
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
let scale = self.scale;
let size = self.tile_size();
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
}
}
+3717 -1331
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -11,7 +11,6 @@ pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layer;
pub mod layout;
pub mod niri;
pub mod protocols;
@@ -28,6 +27,3 @@ pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
#[cfg(test)]
mod tests;
+37 -49
View File
@@ -5,12 +5,13 @@ use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::{CommandFactory, Parser};
use clap::Parser;
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
@@ -32,11 +33,6 @@ use tracing_subscriber::EnvFilter;
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
#[cfg(feature = "profile-with-tracy-allocations")]
#[global_allocator]
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
if env::var_os("RUST_BACKTRACE").is_none() {
@@ -48,14 +44,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_writer(io::stderr)
.with_env_filter(env_filter)
.init();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
@@ -66,12 +54,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let cli = Cli::parse();
if cli.session {
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
// DISPLAY, WAYLAND_DISPLAY or WAYLAND_SOCKET from our environment if they are set, since
// they will cause the winit backend to be selected instead.
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
// the winit backend to be selected instead.
if env::var_os("DISPLAY").is_some() {
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
@@ -80,10 +75,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
if env::var_os("WAYLAND_SOCKET").is_some() {
warn!("running as a session but WAYLAND_SOCKET is set, removing it");
env::remove_var("WAYLAND_SOCKET");
}
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
@@ -91,6 +82,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("XDG_SESSION_TYPE", "wayland");
}
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
@@ -107,10 +101,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
Sub::Panic => cause_panic(),
Sub::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "niri", &mut io::stdout());
return Ok(());
}
}
}
@@ -161,12 +151,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
let config_load_result = Config::load(&path);
let config_errored = config_load_result.is_err();
let mut config = config_load_result
.map_err(|err| warn!("{err:?}"))
let mut config_errored = false;
let mut config = Config::load(&path)
.map_err(|err| {
warn!("{err:?}");
config_errored = true;
})
.unwrap_or_default();
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
@@ -180,13 +179,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
false,
true,
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = state.niri.socket_name.as_deref().unwrap();
let socket_name = &state.niri.socket_name;
env::set_var("WAYLAND_DISPLAY", socket_name);
info!(
"listening on Wayland socket: {}",
@@ -195,9 +192,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
let socket_path = ipc.socket_path.as_deref().unwrap();
env::set_var(SOCKET_PATH_ENV, socket_path);
info!("IPC listening on: {}", socket_path.to_string_lossy());
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
if cli.session {
@@ -230,20 +226,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up config file watcher.
let _watcher = {
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
// watcher thread.
let process = |path: &Path| {
Config::load(path).map_err(|err| {
warn!("{:?}", err.context("error loading config"));
})
};
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(watch_path.clone(), process, tx);
let watcher = Watcher::new(watch_path.clone(), tx);
event_loop
.handle()
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(config) => state.reload_config(config),
.insert_source(rx, move |event, _, state| match event {
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
calloop::channel::Event::Closed => (),
})
.unwrap();
@@ -251,10 +239,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Spawn commands from cli and auto-start.
spawn(cli.command, None);
spawn(cli.command);
for elem in spawn_at_startup {
spawn(elem.command, None);
spawn(elem.command);
}
// Show the config error notification right away if needed.
@@ -353,7 +341,7 @@ fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
let system_path = system_config_path();
if let Some(path) = default_config_path() {
if path.exists() {
return (path.clone(), path, false);
return (path.clone(), path, true);
}
if system_path.exists() {
+642 -1912
View File
File diff suppressed because it is too large Load Diff
+26 -10
View File
@@ -11,7 +11,10 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
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,
};
@@ -19,7 +22,6 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
use crate::utils::with_toplevel_role;
const VERSION: u32 = 3;
@@ -94,23 +96,37 @@ pub fn refresh(state: &mut State) {
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|mapped, output, _| {
let toplevel = mapped.toplevel();
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, role, output, false);
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let toplevel = window.toplevel().expect("no X11 support");
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
});
}
}
+1
View File
@@ -177,6 +177,7 @@ where
}
// Verify that there's no more data.
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
{
match file.read(&mut [0]) {
Ok(0) => (),
-1
View File
@@ -3,6 +3,5 @@ pub mod gamma_control;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
pub mod virtual_pointer;
pub mod raw;
+2 -2
View File
@@ -206,9 +206,9 @@ where
let output_transform = output.current_transform();
let output_physical_size =
output_transform.transform_size(output.current_mode().unwrap().size);
let output_rect = Rectangle::from_size(output_physical_size);
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
let rect = Rectangle::new(Point::from((x, y)), Size::from((width, height)));
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let output_scale = output.current_scale().fractional_scale();
let physical_rect = rect.to_physical_precise_round(output_scale);
-563
View File
@@ -1,563 +0,0 @@
use std::collections::HashSet;
use std::sync::Mutex;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device,
DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent,
PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent,
};
use smithay::input::pointer::AxisFrame;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::protocol::wl_pointer;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use wayland_backend::protocol::WEnum;
use wayland_protocols_wlr::virtual_pointer::v1::server::{
zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1,
};
use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1;
use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1;
const VERSION: u32 = 2;
pub struct VirtualPointerManagerState {
virtual_pointers: HashSet<ZwlrVirtualPointerV1>,
}
pub struct VirtualPointerManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub struct VirtualPointerInputBackend;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct VirtualPointer {
pointer: ZwlrVirtualPointerV1,
}
#[derive(Debug)]
pub struct VirtualPointerUserData {
seat: Option<WlSeat>,
output: Option<Output>,
axis_frame: Mutex<Option<AxisFrame>>,
}
impl VirtualPointer {
fn data(&self) -> &VirtualPointerUserData {
self.pointer.data().unwrap()
}
pub fn seat(&self) -> Option<&WlSeat> {
self.data().seat.as_ref()
}
pub fn output(&self) -> Option<&Output> {
self.data().output.as_ref()
}
fn finish_axis_frame(&self) -> Option<AxisFrame> {
self.data().axis_frame.lock().unwrap().take()
}
fn mutate_axis_frame(&self, time: Option<u32>, f: impl FnOnce(AxisFrame) -> AxisFrame) {
let mut frame = self.data().axis_frame.lock().unwrap();
*frame = frame.or(time.map(AxisFrame::new)).map(f);
}
}
impl Device for VirtualPointer {
fn id(&self) -> String {
format!("wlr virtual pointer {}", self.pointer.id())
}
fn name(&self) -> String {
String::from("virtual pointer")
}
fn has_capability(&self, capability: DeviceCapability) -> bool {
matches!(capability, DeviceCapability::Pointer)
}
fn usb_id(&self) -> Option<(u32, u32)> {
None
}
fn syspath(&self) -> Option<std::path::PathBuf> {
None
}
}
pub struct VirtualPointerMotionEvent {
pointer: VirtualPointer,
time: u32,
dx: f64,
dy: f64,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl PointerMotionEvent<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
fn delta_x(&self) -> f64 {
self.dx
}
fn delta_y(&self) -> f64 {
self.dy
}
fn delta_x_unaccel(&self) -> f64 {
self.dx
}
fn delta_y_unaccel(&self) -> f64 {
self.dy
}
}
pub struct VirtualPointerMotionAbsoluteEvent {
pointer: VirtualPointer,
time: u32,
x: u32,
y: u32,
x_extent: u32,
y_extent: u32,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl AbsolutePositionEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
fn x(&self) -> f64 {
self.x as f64 / self.x_extent as f64
}
fn y(&self) -> f64 {
self.y as f64 / self.y_extent as f64
}
fn x_transformed(&self, width: i32) -> f64 {
(self.x as i64 * width as i64) as f64 / self.x_extent as f64
}
fn y_transformed(&self, height: i32) -> f64 {
(self.y as i64 * height as i64) as f64 / self.y_extent as f64
}
}
pub struct VirtualPointerButtonEvent {
pointer: VirtualPointer,
time: u32,
button: u32,
state: ButtonState,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl PointerButtonEvent<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
fn button_code(&self) -> u32 {
self.button
}
fn state(&self) -> ButtonState {
self.state
}
}
pub struct VirtualPointerAxisEvent {
pointer: VirtualPointer,
frame: AxisFrame,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
fn time(&self) -> u64 {
self.frame.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
fn tuple_axis<T>(tuple: (T, T), axis: Axis) -> T {
match axis {
Axis::Horizontal => tuple.0,
Axis::Vertical => tuple.1,
}
}
impl PointerAxisEvent<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
fn amount(&self, axis: Axis) -> Option<f64> {
Some(tuple_axis(self.frame.axis, axis))
}
fn amount_v120(&self, axis: Axis) -> Option<f64> {
self.frame.v120.map(|v120| tuple_axis(v120, axis) as f64)
}
fn source(&self) -> AxisSource {
self.frame.source.unwrap_or_else(|| {
warn!("AxisSource: no source set, giving bogus value");
AxisSource::Continuous
})
}
fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection {
tuple_axis(self.frame.relative_direction, axis)
}
}
impl PointerMotionAbsoluteEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {}
impl InputBackend for VirtualPointerInputBackend {
type Device = VirtualPointer;
type KeyboardKeyEvent = UnusedEvent;
type PointerAxisEvent = VirtualPointerAxisEvent;
type PointerButtonEvent = VirtualPointerButtonEvent;
type PointerMotionEvent = VirtualPointerMotionEvent;
type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
pub trait VirtualPointerHandler {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState;
fn create_virtual_pointer(&mut self, pointer: VirtualPointer) {
let _ = pointer;
}
fn destroy_virtual_pointer(&mut self, pointer: VirtualPointer) {
let _ = pointer;
}
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent);
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent);
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent);
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent);
}
impl VirtualPointerManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = VirtualPointerManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrVirtualPointerManagerV1, _>(VERSION, global_data);
Self {
virtual_pointers: HashSet::new(),
}
}
}
impl<D> GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData, D>
for VirtualPointerManagerState
where
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<ZwlrVirtualPointerManagerV1>,
_manager_state: &VirtualPointerManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &VirtualPointerManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrVirtualPointerManagerV1, (), D> for VirtualPointerManagerState
where
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
_resource: &ZwlrVirtualPointerManagerV1,
request: <ZwlrVirtualPointerManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let (id, seat, output) = match request {
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => {
(id, seat, None)
}
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput {
seat,
output,
id,
} => (id, seat, output.as_ref().and_then(Output::from_resource)),
zwlr_virtual_pointer_manager_v1::Request::Destroy => return,
_ => unreachable!(),
};
let pointer = data_init.init(
id,
VirtualPointerUserData {
seat,
output,
axis_frame: Mutex::new(None),
},
);
state
.virtual_pointer_manager_state()
.virtual_pointers
.insert(pointer.clone());
state.create_virtual_pointer(VirtualPointer { pointer });
}
}
impl<D> Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData, D> for VirtualPointerManagerState
where
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn request(
handler: &mut D,
_client: &Client,
resource: &ZwlrVirtualPointerV1,
request: <ZwlrVirtualPointerV1 as Resource>::Request,
_data: &VirtualPointerUserData,
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let pointer = VirtualPointer {
pointer: resource.clone(),
};
match request {
zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => {
let event = VirtualPointerMotionEvent {
pointer,
time,
dx,
dy,
};
handler.on_virtual_pointer_motion(event);
}
zwlr_virtual_pointer_v1::Request::MotionAbsolute {
time,
x,
y,
x_extent,
y_extent,
} => {
let event = VirtualPointerMotionAbsoluteEvent {
pointer,
time,
x,
y,
x_extent,
y_extent,
};
handler.on_virtual_pointer_motion_absolute(event);
}
zwlr_virtual_pointer_v1::Request::Button {
time,
button,
state,
} => {
// state is an enum but wlroots treats it as a C boolean (zero or nonzero)
// so we emulate that behaviour too. ButtonState::Pressed and any invalid value
// counts as pressed.
// https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/3187479c07c34a4de82c06a316a763a36a0499da/types/wlr_virtual_pointer_v1.c#L74
let state = match state {
WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released,
_ => ButtonState::Pressed,
};
let event = VirtualPointerButtonEvent {
pointer,
time,
button,
state,
};
handler.on_virtual_pointer_button(event);
}
zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("Axis: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| frame.value(axis, value));
}
zwlr_virtual_pointer_v1::Request::Frame => {
if let Some(frame) = pointer.finish_axis_frame() {
let event = VirtualPointerAxisEvent { pointer, frame };
handler.on_virtual_pointer_axis(event);
}
}
zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => {
let axis_source = match axis_source {
WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel,
WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger,
WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous,
WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt,
_ => {
warn!("AxisSource: invalid axis source");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxisSource,
"invalid axis source",
);
return;
}
};
pointer.mutate_axis_frame(None, |frame| frame.source(axis_source));
}
zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("AxisStop: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| frame.stop(axis));
}
zwlr_virtual_pointer_v1::Request::AxisDiscrete {
time,
axis,
value,
discrete,
} => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("AxisDiscrete: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| {
frame.value(axis, value).v120(axis, discrete)
});
}
zwlr_virtual_pointer_v1::Request::Destroy => {}
_ => unreachable!(),
}
}
fn destroyed(
handler: &mut D,
_client: wayland_backend::server::ClientId,
resource: &ZwlrVirtualPointerV1,
_data: &VirtualPointerUserData,
) {
let pointer = VirtualPointer {
pointer: resource.clone(),
};
handler.destroy_virtual_pointer(pointer);
handler
.virtual_pointer_manager_state()
.virtual_pointers
.remove(resource);
}
}
#[macro_export]
macro_rules! delegate_virtual_pointer {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: ()
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerUserData
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
};
}
+36 -106
View File
@@ -11,7 +11,7 @@ use anyhow::Context as _;
use calloop::timer::{TimeoutAction, Timer};
use calloop::RegistrationToken;
use pipewire::context::Context;
use pipewire::core::{Core, PW_ID_CORE};
use pipewire::core::Core;
use pipewire::main_loop::MainLoop;
use pipewire::properties::Properties;
use pipewire::spa::buffer::DataType;
@@ -36,16 +36,16 @@ use smithay::backend::drm::DrmDeviceFd;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{Output, OutputModeSource};
use smithay::output::{Output, OutputModeSource, WeakOutput};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Scale, Size, Transform};
use zbus::object_server::SignalEmitter;
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::{CastTarget, State};
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
use crate::niri::State;
use crate::render_helpers::render_to_dmabuf;
use crate::utils::get_monotonic_time;
// Give a 0.1 ms allowance for presentation time errors.
@@ -54,24 +54,20 @@ const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
pub struct PipeWire {
_context: Context,
pub core: Core,
pub token: RegistrationToken,
to_niri: calloop::channel::Sender<PwToNiri>,
}
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw { stream_id: usize },
FatalError,
Redraw(CastTarget),
}
pub struct Cast {
pub session_id: usize,
pub stream_id: usize,
pub stream: Stream,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub target: CastTarget,
pub dynamic_target: bool,
formats: FormatSet,
state: Rc<RefCell<CastState>>,
refresh: Rc<Cell<u32>>,
@@ -83,7 +79,6 @@ pub struct Cast {
scheduled_redraw: Option<RegistrationToken>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum CastState {
ResizePending {
@@ -111,6 +106,12 @@ pub enum CastSizeChange {
Pending,
}
#[derive(Clone, PartialEq, Eq)]
pub enum CastTarget {
Output(WeakOutput),
Window { id: u64 },
}
macro_rules! make_params {
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
let mut b1 = Vec::new();
@@ -133,26 +134,15 @@ macro_rules! make_params {
}
impl PipeWire {
pub fn new(
event_loop: &LoopHandle<'static, State>,
to_niri: calloop::channel::Sender<PwToNiri>,
) -> anyhow::Result<Self> {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
let to_niri_ = to_niri.clone();
let listener = core
.add_listener_local()
.error(move |id, seq, res, message| {
.error(|id, seq, res, message| {
warn!(id, seq, res, message, "pw error");
// Reset PipeWire on connection errors.
if id == PW_ID_CORE && res == -32 {
if let Err(err) = to_niri_.send(PwToNiri::FatalError) {
warn!("error sending FatalError to niri: {err:?}");
}
}
})
.register();
mem::forget(listener);
@@ -164,7 +154,7 @@ impl PipeWire {
}
}
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
let token = event_loop
event_loop
.insert_source(generic, move |_, wrapper, _| {
let _span = tracy_client::span!("pipewire iteration");
wrapper.0.loop_().iterate(Duration::ZERO);
@@ -172,10 +162,17 @@ impl PipeWire {
})
.unwrap();
let (to_niri, from_pipewire) = calloop::channel::channel();
event_loop
.insert_source(from_pipewire, move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
calloop::channel::Event::Closed => (),
})
.unwrap();
Ok(Self {
_context: context,
core,
token,
to_niri,
})
}
@@ -186,14 +183,12 @@ impl PipeWire {
gbm: GbmDevice<DrmDeviceFd>,
formats: FormatSet,
session_id: usize,
stream_id: usize,
target: CastTarget,
dynamic_target: bool,
size: Size<i32, Physical>,
refresh: u32,
alpha: bool,
cursor_mode: CursorMode,
signal_ctx: SignalEmitter<'static>,
signal_ctx: SignalContext<'static>,
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
@@ -203,9 +198,10 @@ impl PipeWire {
warn!("error sending StopCast to niri: {err:?}");
}
};
let target_ = target.clone();
let to_niri_ = self.to_niri.clone();
let redraw = move || {
if let Err(err) = to_niri_.send(PwToNiri::Redraw { stream_id }) {
if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
warn!("error sending Redraw to niri: {err:?}");
}
};
@@ -649,12 +645,10 @@ impl PipeWire {
let cast = Cast {
session_id,
stream_id,
stream,
_listener: listener,
is_active,
target,
dynamic_target,
formats,
state,
refresh,
@@ -775,11 +769,7 @@ impl Cast {
let timer = Timer::from_duration(duration);
let token = event_loop
.insert_source(timer, move |_, _, state| {
// Guard against output disconnecting before the timer has a chance to run.
if state.niri.output_state.contains_key(&output) {
state.niri.queue_redraw(&output);
}
state.niri.queue_redraw(&output);
TimeoutAction::Drop
})
.unwrap();
@@ -822,7 +812,6 @@ impl Cast {
elements: &[impl RenderElement<GlesRenderer>],
size: Size<i32, Physical>,
scale: Scale<f64>,
wait_for_sync: bool,
) -> bool {
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
error!("cast must be in Ready state to render");
@@ -845,15 +834,18 @@ impl Cast {
return false;
}
let Some(mut buffer) = self.stream.dequeue_buffer() else {
warn!("no available buffer in pw stream, skipping frame");
return false;
let mut buffer = match self.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
warn!("no available buffer in pw stream, skipping frame");
return false;
}
};
let fd = buffer.datas_mut()[0].as_raw().fd;
let dmabuf = &self.dmabufs.borrow()[&fd];
match render_to_dmabuf(
if let Err(err) = render_to_dmabuf(
renderer,
dmabuf.clone(),
size,
@@ -861,70 +853,8 @@ impl Cast {
Transform::Normal,
elements.iter().rev(),
) {
Ok(sync_point) => {
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
if wait_for_sync {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = sync_point.wait() {
warn!("error waiting for pw frame completion: {err:?}");
}
}
}
Err(err) => {
warn!("error rendering to dmabuf: {err:?}");
return false;
}
}
for (data, (stride, offset)) in
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
{
let chunk = data.chunk_mut();
*chunk.size_mut() = 1;
*chunk.stride_mut() = stride as i32;
*chunk.offset_mut() = offset;
trace!(
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
data.as_raw().fd
);
}
true
}
pub fn dequeue_buffer_and_clear(
&mut self,
renderer: &mut GlesRenderer,
wait_for_sync: bool,
) -> bool {
// Clear out the damage tracker if we're in Ready state.
if let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() {
*damage_tracker = None;
};
let Some(mut buffer) = self.stream.dequeue_buffer() else {
warn!("no available buffer in pw stream, skipping clear");
warn!("error rendering to dmabuf: {err:?}");
return false;
};
let fd = buffer.datas_mut()[0].as_raw().fd;
let dmabuf = &self.dmabufs.borrow()[&fd];
match clear_dmabuf(renderer, dmabuf.clone()) {
Ok(sync_point) => {
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
if wait_for_sync {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = sync_point.wait() {
warn!("error waiting for pw frame completion: {err:?}");
}
}
}
Err(err) => {
warn!("error clearing dmabuf: {err:?}");
return false;
}
}
for (data, (stride, offset)) in
@@ -1093,7 +1023,7 @@ fn allocate_buffer(
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
.context("error creating GBM buffer object")?;
let modifier = bo.modifier();
let modifier = bo.modifier().unwrap();
let buffer = GbmBuffer::from_bo(bo, false);
Ok((buffer, modifier))
}

Some files were not shown because too many files have changed in this diff Show More