Compare commits

..

5 Commits

Author SHA1 Message Date
Ivan Molodetskikh 75c79116a7 Bump version to 0.1.10-1
Uhh, apparently cargo doesn't like four-component versions.
2024-11-13 10:39:54 +03:00
Ivan Molodetskikh 4f44ef081f Guard against closed screenshot UI in its binds
They can trigger with closed screenshot UI via key repeat.
2024-11-13 10:24:21 +03:00
Ramses 4fc76b50d0 Unhide the pointer on scroll events (#797)
* Unhide the pointer on scroll events

Since we reset the surface under the pointer when we hide the pointer
(see update_pointer_contents), scroll events don't work when the pointer
is hidden.
So to make scrolling work, we make sure that we unhide the pointer when
a scrolling event occurs.

* Update src/input/mod.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-13 10:24:06 +03:00
Ivan Molodetskikh e1f065ac23 Start interactive move on Mod+Touch 2024-11-13 10:23:51 +03:00
Ivan Molodetskikh 7cc10ce1b5 Fix scrolling not working with missing mouse config 2024-11-13 10:23:44 +03:00
3252 changed files with 7028 additions and 57454 deletions
-22
View File
@@ -1,22 +0,0 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
groups:
smithay:
patterns:
- "smithay"
- "smithay-drm-extras"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
+26 -22
View File
@@ -23,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
@@ -32,8 +33,8 @@ jobs:
- 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
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
@@ -62,7 +63,7 @@ jobs:
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build tests
- name: Build Tests
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
@@ -73,7 +74,8 @@ jobs:
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
@@ -82,8 +84,8 @@ jobs:
- 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
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
@@ -96,8 +98,9 @@ jobs:
strategy:
fail-fast: false
name: 'msrv - 1.80.1'
runs-on: ubuntu-24.04
name: 'msrv - 1.77.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -106,10 +109,10 @@ jobs:
- 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
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
@@ -120,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
@@ -129,8 +133,8 @@ jobs:
- 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
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:
@@ -142,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
@@ -157,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
@@ -168,13 +172,13 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libdisplay-info-devel 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:
@@ -196,7 +200,7 @@ jobs:
needs: build
permissions:
contents: write
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
@@ -208,7 +212,7 @@ jobs:
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
+904 -595
View File
File diff suppressed because it is too large Load Diff
+34 -44
View File
@@ -1,29 +1,25 @@
[workspace]
members = [
"niri-config",
"niri-ipc",
"niri-visual-tests",
]
members = ["niri-visual-tests"]
[workspace.package]
version = "25.1.0"
version = "0.1.10-1"
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"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.95"
bitflags = "2.7.0"
clap = { version = "4.5.26", features = ["derive"] }
insta = "1.42.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracy-client = { version = "0.18.0", default-features = false }
anyhow = "1.0.93"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.214", 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]
git = "https://github.com/Smithay/smithay.git"
@@ -51,31 +47,32 @@ 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.21.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"] }
directories = "5.0.1"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
fastrand = "2.2.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.29.2"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.169"
libdisplay-info = "0.2.2"
libc = "0.2.162"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.1.0", path = "niri-config" }
niri-ipc = { version = "25.1.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "4.6.0"
pango = { version = "0.20.7", features = ["v1_44"] }
pangocairo = "0.20.7"
niri-config = { version = "0.1.10-1", path = "niri-config" }
niri-ipc = { version = "0.1.10-1", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.5.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.10.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.3"
serde.workspace = true
@@ -84,11 +81,11 @@ smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.4", optional = true }
url = { version = "2.5.3", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
xcursor = "0.3.8"
zbus = { version = "5.2.0", optional = true }
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -110,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.7"
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.
@@ -144,12 +138,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.01"
version = "0.1.10.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+5 -33
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/d142e57d-a25d-4ddb-ab46-311417458211)
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
## About
@@ -28,7 +28,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
@@ -45,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) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
@@ -103,6 +78,3 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[gamescope]: https://github.com/ValveSoftware/gamescope
Generated
+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": 1733064805,
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1733106880,
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
"lastModified": 1727663505,
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
"type": "github"
},
"original": {
+8 -11
View File
@@ -26,8 +26,10 @@
{
lib,
cairo,
clang,
dbus,
libGL,
libclang,
libdisplay-info,
libinput,
seatd,
@@ -77,7 +79,7 @@
strictDeps = true;
nativeBuildInputs = [
rustPlatform.bindgenHook
clang
pkg-config
];
@@ -106,15 +108,6 @@
++ 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 =
''
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
@@ -126,6 +119,8 @@
'';
env = {
LIBCLANG_PATH = lib.getLib libclang + "/lib";
# Force linking with libEGL and libwayland-client
# so they can be discovered by `dlopen()`
RUSTFLAGS = toString (
@@ -196,7 +191,7 @@
];
nativeBuildInputs = [
pkgs.rustPlatform.bindgenHook
pkgs.clang
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
@@ -206,6 +201,8 @@
];
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
+2 -2
View File
@@ -12,13 +12,13 @@ bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "25.1.0", path = "../niri-ipc" }
niri-ipc = { version = "0.1.10-1", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
insta.workspace = true
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
-22
View File
@@ -1,22 +0,0 @@
use crate::{BlockOutFrom, RegexEq};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
#[knuffel(children(name = "match"))]
pub matches: Vec<Match>,
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
+39 -318
View File
@@ -10,12 +10,9 @@ use std::time::Duration;
use bitflags::bitflags;
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use layer_rule::LayerRule;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{
ConfiguredMode, LayoutSwitchTarget, PositionChange, SizeChange, Transform,
WorkspaceReferenceArg,
};
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
use regex::Regex;
use smithay::backend::renderer::Color32F;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@@ -24,11 +21,6 @@ use smithay::reexports::input;
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);
pub mod layer_rule;
mod utils;
pub use utils::RegexEq;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
#[knuffel(child, default)]
@@ -59,8 +51,6 @@ pub struct Config {
pub environment: Environment,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
#[knuffel(children(name = "layer-rule"))]
pub layer_rules: Vec<LayerRule>,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
@@ -447,8 +437,6 @@ pub struct Layout {
pub center_focused_column: CenterFocusedColumn,
#[knuffel(child)]
pub always_center_single_column: bool,
#[knuffel(child)]
pub empty_workspace_above_first: bool,
#[knuffel(child, unwrap(argument), default = Self::default().gaps)]
pub gaps: FloatOrInt<0, 65535>,
#[knuffel(child, default)]
@@ -465,7 +453,6 @@ impl Default for Layout {
default_column_width: Default::default(),
center_focused_column: Default::default(),
always_center_single_column: false,
empty_workspace_above_first: false,
gaps: FloatOrInt(16.),
struts: Default::default(),
preset_window_heights: Default::default(),
@@ -708,7 +695,7 @@ pub enum PresetSize {
Fixed(#[knuffel(argument)] i32),
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct DefaultPresetSize(pub Option<PresetSize>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
@@ -978,8 +965,6 @@ pub struct WindowRule {
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child)]
pub default_window_height: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child, unwrap(argument))]
@@ -988,10 +973,6 @@ pub struct WindowRule {
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_floating: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_focused: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
@@ -1019,16 +1000,15 @@ pub struct WindowRule {
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, unwrap(argument))]
pub variable_refresh_rate: Option<bool>,
#[knuffel(child)]
pub default_floating_position: Option<FoIPosition>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
// Remember to update the PartialEq impl when adding fields!
#[derive(knuffel::Decode, Debug, Default, Clone)]
pub struct Match {
#[knuffel(property, str)]
pub app_id: Option<RegexEq>,
pub app_id: Option<Regex>,
#[knuffel(property, str)]
pub title: Option<RegexEq>,
pub title: Option<Regex>,
#[knuffel(property)]
pub is_active: Option<bool>,
#[knuffel(property)]
@@ -1036,11 +1016,20 @@ pub struct Match {
#[knuffel(property)]
pub is_active_in_column: Option<bool>,
#[knuffel(property)]
pub is_floating: Option<bool>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
impl PartialEq for Match {
fn eq(&self, other: &Self) -> bool {
self.is_active == other.is_active
&& self.is_focused == other.is_focused
&& self.is_active_in_column == other.is_active_in_column
&& self.at_startup == other.at_startup
&& self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str)
&& self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct CornerRadius {
pub top_left: f32,
@@ -1084,25 +1073,6 @@ pub struct BorderRule {
pub inactive_gradient: Option<Gradient>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FoIPosition {
#[knuffel(property)]
pub x: FloatOrInt<-65535, 65535>,
#[knuffel(property)]
pub y: FloatOrInt<-65535, 65535>,
#[knuffel(property, default)]
pub relative_to: RelativeTo,
}
#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum RelativeTo {
#[default]
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Default, PartialEq)]
pub struct Binds(pub Vec<Bind>);
@@ -1124,11 +1094,6 @@ pub struct Key {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Trigger {
Keysym(Keysym),
MouseLeft,
MouseRight,
MouseMiddle,
MouseBack,
MouseForward,
WheelScrollDown,
WheelScrollUp,
WheelScrollLeft,
@@ -1203,7 +1168,6 @@ pub enum Action {
FullscreenWindowById(u64),
#[knuffel(skip)]
FocusWindow(u64),
FocusWindowPrevious,
FocusColumnLeft,
FocusColumnRight,
FocusColumnFirst,
@@ -1240,12 +1204,7 @@ pub enum Action {
ConsumeOrExpelWindowRightById(u64),
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
SwapWindowLeft,
SwapWindowRight,
CenterColumn,
CenterWindow,
#[knuffel(skip)]
CenterWindowById(u64),
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
@@ -1263,39 +1222,18 @@ pub enum Action {
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveWorkspaceDown,
MoveWorkspaceUp,
SetWorkspaceName(#[knuffel(argument)] String),
#[knuffel(skip)]
SetWorkspaceNameByRef {
name: String,
reference: WorkspaceReference,
},
UnsetWorkspaceName,
#[knuffel(skip)]
UnsetWorkSpaceNameByRef(#[knuffel(argument)] WorkspaceReference),
FocusMonitorLeft,
FocusMonitorRight,
FocusMonitorDown,
FocusMonitorUp,
FocusMonitorPrevious,
FocusMonitorNext,
MoveWindowToMonitorLeft,
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
MoveWindowToMonitorPrevious,
MoveWindowToMonitorNext,
MoveColumnToMonitorLeft,
MoveColumnToMonitorRight,
MoveColumnToMonitorDown,
MoveColumnToMonitorUp,
MoveColumnToMonitorPrevious,
MoveColumnToMonitorNext,
SetWindowWidth(#[knuffel(argument, str)] SizeChange),
#[knuffel(skip)]
SetWindowWidthById {
id: u64,
change: SizeChange,
},
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
#[knuffel(skip)]
SetWindowHeightById {
@@ -1306,9 +1244,6 @@ pub enum Action {
#[knuffel(skip)]
ResetWindowHeightById(u64),
SwitchPresetColumnWidth,
SwitchPresetWindowWidth,
#[knuffel(skip)]
SwitchPresetWindowWidthById(u64),
SwitchPresetWindowHeight,
#[knuffel(skip)]
SwitchPresetWindowHeightById(u64),
@@ -1320,26 +1255,6 @@ pub enum Action {
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorUp,
MoveWorkspaceToMonitorPrevious,
MoveWorkspaceToMonitorNext,
ToggleWindowFloating,
#[knuffel(skip)]
ToggleWindowFloatingById(u64),
MoveWindowToFloating,
#[knuffel(skip)]
MoveWindowToFloatingById(u64),
MoveWindowToTiling,
#[knuffel(skip)]
MoveWindowToTilingById(u64),
FocusFloating,
FocusTiling,
SwitchFocusBetweenFloatingAndTiling,
#[knuffel(skip)]
MoveFloatingWindowById {
id: Option<u64>,
x: PositionChange,
y: PositionChange,
},
}
impl From<niri_ipc::Action> for Action {
@@ -1359,7 +1274,6 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow,
niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id),
niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id),
niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious,
niri_ipc::Action::FocusColumnLeft {} => Self::FocusColumnLeft,
niri_ipc::Action::FocusColumnRight {} => Self::FocusColumnRight,
niri_ipc::Action::FocusColumnFirst {} => Self::FocusColumnFirst,
@@ -1408,11 +1322,7 @@ impl From<niri_ipc::Action> for Action {
}
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight,
niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft,
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
niri_ipc::Action::FocusWorkspaceDown {} => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp {} => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { reference } => {
@@ -1439,44 +1349,18 @@ impl From<niri_ipc::Action> for Action {
}
niri_ipc::Action::MoveWorkspaceDown {} => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp {} => Self::MoveWorkspaceUp,
niri_ipc::Action::SetWorkspaceName {
name,
workspace: None,
} => Self::SetWorkspaceName(name),
niri_ipc::Action::SetWorkspaceName {
name,
workspace: Some(reference),
} => Self::SetWorkspaceNameByRef {
name,
reference: WorkspaceReference::from(reference),
},
niri_ipc::Action::UnsetWorkspaceName { reference: None } => Self::UnsetWorkspaceName,
niri_ipc::Action::UnsetWorkspaceName {
reference: Some(reference),
} => Self::UnsetWorkSpaceNameByRef(WorkspaceReference::from(reference)),
niri_ipc::Action::FocusMonitorLeft {} => Self::FocusMonitorLeft,
niri_ipc::Action::FocusMonitorRight {} => Self::FocusMonitorRight,
niri_ipc::Action::FocusMonitorDown {} => Self::FocusMonitorDown,
niri_ipc::Action::FocusMonitorUp {} => Self::FocusMonitorUp,
niri_ipc::Action::FocusMonitorPrevious {} => Self::FocusMonitorPrevious,
niri_ipc::Action::FocusMonitorNext {} => Self::FocusMonitorNext,
niri_ipc::Action::MoveWindowToMonitorLeft {} => Self::MoveWindowToMonitorLeft,
niri_ipc::Action::MoveWindowToMonitorRight {} => Self::MoveWindowToMonitorRight,
niri_ipc::Action::MoveWindowToMonitorDown {} => Self::MoveWindowToMonitorDown,
niri_ipc::Action::MoveWindowToMonitorUp {} => Self::MoveWindowToMonitorUp,
niri_ipc::Action::MoveWindowToMonitorPrevious {} => Self::MoveWindowToMonitorPrevious,
niri_ipc::Action::MoveWindowToMonitorNext {} => Self::MoveWindowToMonitorNext,
niri_ipc::Action::MoveColumnToMonitorLeft {} => Self::MoveColumnToMonitorLeft,
niri_ipc::Action::MoveColumnToMonitorRight {} => Self::MoveColumnToMonitorRight,
niri_ipc::Action::MoveColumnToMonitorDown {} => Self::MoveColumnToMonitorDown,
niri_ipc::Action::MoveColumnToMonitorUp {} => Self::MoveColumnToMonitorUp,
niri_ipc::Action::MoveColumnToMonitorPrevious {} => Self::MoveColumnToMonitorPrevious,
niri_ipc::Action::MoveColumnToMonitorNext {} => Self::MoveColumnToMonitorNext,
niri_ipc::Action::SetWindowWidth { id: None, change } => Self::SetWindowWidth(change),
niri_ipc::Action::SetWindowWidth {
id: Some(id),
change,
} => Self::SetWindowWidthById { id, change },
niri_ipc::Action::SetWindowHeight { id: None, change } => Self::SetWindowHeight(change),
niri_ipc::Action::SetWindowHeight {
id: Some(id),
@@ -1485,10 +1369,6 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight,
niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id),
niri_ipc::Action::SwitchPresetColumnWidth {} => Self::SwitchPresetColumnWidth,
niri_ipc::Action::SwitchPresetWindowWidth { id: None } => Self::SwitchPresetWindowWidth,
niri_ipc::Action::SwitchPresetWindowWidth { id: Some(id) } => {
Self::SwitchPresetWindowWidthById(id)
}
niri_ipc::Action::SwitchPresetWindowHeight { id: None } => {
Self::SwitchPresetWindowHeight
}
@@ -1503,33 +1383,9 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::MoveWorkspaceToMonitorRight {} => Self::MoveWorkspaceToMonitorRight,
niri_ipc::Action::MoveWorkspaceToMonitorDown {} => Self::MoveWorkspaceToMonitorDown,
niri_ipc::Action::MoveWorkspaceToMonitorUp {} => Self::MoveWorkspaceToMonitorUp,
niri_ipc::Action::MoveWorkspaceToMonitorPrevious {} => {
Self::MoveWorkspaceToMonitorPrevious
}
niri_ipc::Action::MoveWorkspaceToMonitorNext {} => Self::MoveWorkspaceToMonitorNext,
niri_ipc::Action::ToggleDebugTint {} => Self::ToggleDebugTint,
niri_ipc::Action::DebugToggleOpaqueRegions {} => Self::DebugToggleOpaqueRegions,
niri_ipc::Action::DebugToggleDamage {} => Self::DebugToggleDamage,
niri_ipc::Action::ToggleWindowFloating { id: None } => Self::ToggleWindowFloating,
niri_ipc::Action::ToggleWindowFloating { id: Some(id) } => {
Self::ToggleWindowFloatingById(id)
}
niri_ipc::Action::MoveWindowToFloating { id: None } => Self::MoveWindowToFloating,
niri_ipc::Action::MoveWindowToFloating { id: Some(id) } => {
Self::MoveWindowToFloatingById(id)
}
niri_ipc::Action::MoveWindowToTiling { id: None } => Self::MoveWindowToTiling,
niri_ipc::Action::MoveWindowToTiling { id: Some(id) } => {
Self::MoveWindowToTilingById(id)
}
niri_ipc::Action::FocusFloating {} => Self::FocusFloating,
niri_ipc::Action::FocusTiling {} => Self::FocusTiling,
niri_ipc::Action::SwitchFocusBetweenFloatingAndTiling {} => {
Self::SwitchFocusBetweenFloatingAndTiling
}
niri_ipc::Action::MoveFloatingWindow { id, x, y } => {
Self::MoveFloatingWindowById { id, x, y }
}
}
}
}
@@ -1669,13 +1525,9 @@ pub struct DebugConfig {
pub disable_cursor_plane: bool,
#[knuffel(child)]
pub disable_direct_scanout: bool,
#[knuffel(child)]
pub restrict_primary_scanout_to_matching_format: bool,
#[knuffel(child, unwrap(argument))]
pub render_drm_device: Option<PathBuf>,
#[knuffel(child)]
pub force_pipewire_invalid_modifier: bool,
#[knuffel(child)]
pub emulate_zero_presentation_time: bool,
#[knuffel(child)]
pub disable_resize_throttling: bool,
@@ -1685,8 +1537,6 @@ pub struct DebugConfig {
pub keep_laptop_panel_on_when_lid_is_closed: bool,
#[knuffel(child)]
pub disable_monitor_names: bool,
#[knuffel(child)]
pub strict_new_window_focus_policy: bool,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
@@ -1735,15 +1585,8 @@ impl Default for Config {
impl BorderRule {
pub fn merge_with(&mut self, other: &Self) {
if other.off {
self.off = true;
self.on = false;
}
if other.on {
self.off = false;
self.on = true;
}
self.off |= other.off;
self.on |= other.on;
if let Some(x) = other.width {
self.width = Some(x);
@@ -2977,17 +2820,7 @@ impl FromStr for Key {
}
}
let trigger = if key.eq_ignore_ascii_case("MouseLeft") {
Trigger::MouseLeft
} else if key.eq_ignore_ascii_case("MouseRight") {
Trigger::MouseRight
} else if key.eq_ignore_ascii_case("MouseMiddle") {
Trigger::MouseMiddle
} else if key.eq_ignore_ascii_case("MouseBack") {
Trigger::MouseBack
} else if key.eq_ignore_ascii_case("MouseForward") {
Trigger::MouseForward
} else if key.eq_ignore_ascii_case("WheelScrollDown") {
let trigger = if key.eq_ignore_ascii_case("WheelScrollDown") {
Trigger::WheelScrollDown
} else if key.eq_ignore_ascii_case("WheelScrollUp") {
Trigger::WheelScrollUp
@@ -3096,8 +2929,7 @@ pub fn set_miette_hook() -> Result<(), miette::InstallError> {
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
use niri_ipc::PositionChange;
use k9::snapshot;
use pretty_assertions::assert_eq;
use super::*;
@@ -3285,10 +3117,6 @@ mod tests {
open-on-output "eDP-1"
open-maximized true
open-fullscreen false
open-floating false
open-focused true
default-window-height { fixed 500; }
default-floating-position x=100 y=-200 relative-to="bottom-left"
focus-ring {
off
@@ -3301,11 +3129,6 @@ mod tests {
}
}
layer-rule {
match namespace="^notifications$"
block-out-from "screencast"
}
binds {
Mod+T allow-when-locked=true { spawn "alacritty"; }
Mod+Q { close-window; }
@@ -3485,7 +3308,6 @@ mod tests {
},
center_focused_column: CenterFocusedColumn::OnOverflow,
always_center_single_column: false,
empty_workspace_above_first: false,
},
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
@@ -3539,22 +3361,20 @@ mod tests {
]),
window_rules: vec![WindowRule {
matches: vec![Match {
app_id: Some(RegexEq::from_str(".*alacritty").unwrap()),
app_id: Some(Regex::new(".*alacritty").unwrap()),
title: None,
is_active: None,
is_focused: None,
is_active_in_column: None,
is_floating: None,
at_startup: None,
}],
excludes: vec![
Match {
app_id: None,
title: Some(RegexEq::from_str("~").unwrap()),
title: Some(Regex::new("~").unwrap()),
is_active: None,
is_focused: None,
is_active_in_column: None,
is_floating: None,
at_startup: None,
},
Match {
@@ -3563,21 +3383,12 @@ mod tests {
is_active: Some(true),
is_focused: Some(false),
is_active_in_column: None,
is_floating: None,
at_startup: None,
},
],
open_on_output: Some("eDP-1".to_owned()),
open_maximized: Some(true),
open_fullscreen: Some(false),
open_floating: Some(false),
open_focused: Some(true),
default_window_height: Some(DefaultPresetSize(Some(PresetSize::Fixed(500)))),
default_floating_position: Some(FoIPosition {
x: FloatOrInt(100.),
y: FloatOrInt(-200.),
relative_to: RelativeTo::BottomLeft,
}),
focus_ring: BorderRule {
off: true,
width: Some(FloatOrInt(3.)),
@@ -3590,17 +3401,6 @@ mod tests {
},
..Default::default()
}],
layer_rules: vec![
LayerRule {
matches: vec![layer_rule::Match {
namespace: Some(RegexEq::from_str("^notifications$").unwrap()),
at_startup: None,
}],
excludes: vec![],
opacity: None,
block_out_from: Some(BlockOutFrom::Screencast),
}
],
workspaces: vec![
Workspace {
name: WorkspaceName("workspace-1".to_string()),
@@ -3797,28 +3597,6 @@ mod tests {
assert!("10% ".parse::<SizeChange>().is_err());
}
#[test]
fn parse_position_change() {
assert_eq!(
"10".parse::<PositionChange>().unwrap(),
PositionChange::SetFixed(10.),
);
assert_eq!(
"+10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(10.),
);
assert_eq!(
"-10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(-10.),
);
assert!("10%".parse::<PositionChange>().is_err());
assert!("+10%".parse::<PositionChange>().is_err());
assert!("-10%".parse::<PositionChange>().is_err());
assert!("-".parse::<PositionChange>().is_err());
assert!("10% ".parse::<PositionChange>().is_err());
}
#[test]
fn parse_gradient_interpolation() {
assert_eq!(
@@ -4034,79 +3812,22 @@ mod tests {
)
})
.collect::<Vec<_>>();
assert_debug_snapshot!(
snapshot!(
names,
@r#"
[
"Unknown A A | DP-3",
"A Unknown A | DP-3",
"A A Unknown | DP-3",
"A A A | DP-4",
"A A A | DP-5",
"A A B | DP-3",
"A B A | DP-3",
"B A A | DP-3",
"DP-1 | DP-1",
"DP-2 | DP-2",
]
"#
r#"
[
"Unknown A A | DP-3",
"A Unknown A | DP-3",
"A A Unknown | DP-3",
"A A A | DP-4",
"A A A | DP-5",
"A A B | DP-3",
"A B A | DP-3",
"B A A | DP-3",
"DP-1 | DP-1",
"DP-2 | DP-2",
]
"#
);
}
#[test]
fn test_border_rule_on_off_merging() {
fn is_on(config: &str, rules: &[&str]) -> String {
let mut resolved = BorderRule {
off: false,
on: false,
width: None,
active_color: None,
inactive_color: None,
active_gradient: None,
inactive_gradient: None,
};
for rule in rules.iter().copied() {
let rule = BorderRule {
off: rule == "off" || rule == "off,on",
on: rule == "on" || rule == "off,on",
..Default::default()
};
resolved.merge_with(&rule);
}
let config = Border {
off: config == "off",
..Default::default()
};
if resolved.resolve_against(config).off {
"off"
} else {
"on"
}
.to_owned()
}
assert_snapshot!(is_on("off", &[]), @"off");
assert_snapshot!(is_on("off", &["off"]), @"off");
assert_snapshot!(is_on("off", &["on"]), @"on");
assert_snapshot!(is_on("off", &["off,on"]), @"on");
assert_snapshot!(is_on("on", &[]), @"on");
assert_snapshot!(is_on("on", &["off"]), @"off");
assert_snapshot!(is_on("on", &["on"]), @"on");
assert_snapshot!(is_on("on", &["off,on"]), @"on");
assert_snapshot!(is_on("off", &["off", "off"]), @"off");
assert_snapshot!(is_on("off", &["off", "on"]), @"on");
assert_snapshot!(is_on("off", &["on", "off"]), @"off");
assert_snapshot!(is_on("off", &["on", "on"]), @"on");
assert_snapshot!(is_on("on", &["off", "off"]), @"off");
assert_snapshot!(is_on("on", &["off", "on"]), @"on");
assert_snapshot!(is_on("on", &["on", "off"]), @"off");
assert_snapshot!(is_on("on", &["on", "on"]), @"on");
}
}
-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)
}
}
+1 -1
View File
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.1.0"
niri-ipc = "=0.1.10"
```
+3 -224
View File
@@ -24,7 +24,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.1.0"
//! niri-ipc = "=0.1.10"
//! ```
//!
//! ## Features
@@ -55,8 +55,6 @@ 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.
@@ -121,8 +119,6 @@ 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.
@@ -204,8 +200,6 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus the previously focused window.
FocusWindowPrevious {},
/// Focus the column to the left.
FocusColumnLeft {},
/// Focus the column to the right.
@@ -290,24 +284,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 {},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
#[cfg_attr(
feature = "clap",
clap(about = "Center the focused window on the screen")
)]
CenterWindow {
/// Id of the window to center.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -354,34 +332,6 @@ pub enum Action {
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Set the name of the focused workspace")
)]
SetWorkspaceName {
/// New name for the workspace.
#[cfg_attr(feature = "clap", arg())]
name: String,
/// Reference (index or name) of the workspace to name.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
workspace: Option<WorkspaceReferenceArg>,
},
/// Unset the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Unset the name of the focused workspace")
)]
UnsetWorkspaceName {
/// Reference (index or name) of the workspace to unname.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg())]
reference: Option<WorkspaceReferenceArg>,
},
/// Focus the monitor to the left.
FocusMonitorLeft {},
/// Focus the monitor to the right.
@@ -390,10 +340,6 @@ pub enum Action {
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp {},
/// Focus the previous monitor.
FocusMonitorPrevious {},
/// Focus the next monitor.
FocusMonitorNext {},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
@@ -402,10 +348,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 the focused column to the monitor to the left.
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
@@ -414,26 +356,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 {},
/// 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",
@@ -447,7 +369,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.
@@ -464,14 +386,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.
@@ -485,7 +399,7 @@ 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,
},
/// Switch between keyboard layouts.
@@ -504,69 +418,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 {},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage {},
/// Move the focused window between the floating and the tiling layout.
ToggleWindowFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the floating layout.
MoveWindowToFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the tiling layout.
MoveWindowToTiling {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switches focus to the floating layout.
FocusFloating {},
/// Switches focus to the tiling layout.
FocusTiling {},
/// Toggles the focus between the floating and the tiling layout.
SwitchFocusBetweenFloatingAndTiling {},
/// Move a floating window on screen.
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
MoveFloatingWindow {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the X position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
x: PositionChange,
/// How to change the Y position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
y: PositionChange,
},
}
/// Change in window or column size.
@@ -583,16 +440,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))]
@@ -848,21 +695,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,
}
/// Output configuration change result.
@@ -924,46 +762,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))]
@@ -1093,25 +891,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;
+3 -3
View File
@@ -10,9 +10,9 @@ repository.workspace = true
[dependencies]
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.5", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.1.0", path = ".." }
niri-config = { version = "25.1.0", path = "../niri-config" }
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.10-1", path = ".." }
niri-config = { version = "0.1.10-1", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+16 -7
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,16 +59,16 @@ impl TestCase for GradientAngle {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
+17 -8
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,7 +19,7 @@ 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.),
@@ -41,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. {
@@ -65,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,
@@ -74,7 +83,7 @@ impl TestCase for GradientArea {
));
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::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(
@@ -99,7 +108,7 @@ 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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklab {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +29,16 @@ impl TestCase for GradientOklabAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklchAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklchDecreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklchIncreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklchLonger {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientOklchShorter {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
+6 -6
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,16 +31,16 @@ impl TestCase for GradientSrgb {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +29,16 @@ impl TestCase for GradientSrgbAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +31,16 @@ impl TestCase for GradientSrgbLinear {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
@@ -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,16 +29,16 @@ impl TestCase for GradientSrgbLinearAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::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.,
+32 -68
View File
@@ -1,18 +1,18 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::animation::Clock;
use niri::layout::scrolling::ColumnWidth;
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::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)>;
@@ -20,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 {
@@ -66,23 +63,20 @@ impl Layout {
},
..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(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
@@ -97,8 +91,8 @@ impl Layout {
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| {
@@ -111,8 +105,8 @@ 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| {
@@ -125,8 +119,8 @@ 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(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
@@ -141,8 +135,8 @@ impl Layout {
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(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
@@ -159,24 +153,10 @@ impl Layout {
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,
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);
}
@@ -187,24 +167,11 @@ impl Layout {
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,
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);
}
@@ -234,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(
@@ -264,7 +228,7 @@ impl TestCase for Layout {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output, true)
.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 {
+29 -35
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, Scale, 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(
@@ -114,7 +108,7 @@ impl TestCase for Tile {
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(
+8 -8
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, 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, 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, None);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
+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);
+9 -38
View File
@@ -1,20 +1,18 @@
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;
@@ -23,7 +21,7 @@ mod imp {
use super::*;
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
#[derive(Default)]
pub struct SmithayView {
@@ -32,7 +30,6 @@ mod imp {
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
pub clock: RefCell<Clock>,
}
#[glib::object_subclass]
@@ -128,24 +125,16 @@ mod imp {
let size = self.size.get();
let frame_clock = self.obj().frame_clock().unwrap();
let time = Duration::from_micros(frame_clock.frame_time() as u64);
self.clock.borrow_mut().set_unadjusted(time);
// Create the test case if missing.
let mut case = self.test_case.borrow_mut();
let case = case.get_or_insert_with(|| {
let make = self.make_test_case.get().unwrap();
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);
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
@@ -244,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
}
}
+2 -8
View File
@@ -188,7 +188,7 @@ impl LayoutElement for TestWindow {
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
}
@@ -220,8 +220,6 @@ impl LayoutElement for TestWindow {
fn set_active_in_column(&mut self, _active: bool) {}
fn set_floating(&mut self, _floating: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn configure_intent(&self) -> ConfigureIntent {
@@ -242,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 {
@@ -265,7 +259,7 @@ 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
+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 AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
License: ((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
@@ -103,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.
@@ -111,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
@@ -128,7 +127,6 @@ install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
%if %{with check}
%check
export XDG_RUNTIME_DIR="$(mktemp -d)"
%cargo_test -- --workspace --exclude niri-visual-tests
%endif
+6 -21
View File
@@ -250,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 {
@@ -450,17 +441,15 @@ 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; }
@@ -483,10 +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; }
// 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.
-1
View File
@@ -1,5 +1,4 @@
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk;
org.freedesktop.impl.portal.Notification=gtk;
org.freedesktop.impl.portal.Secret=gnome-keyring;
-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));
}
}
+97 -52
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,
@@ -128,32 +124,23 @@ impl Animation {
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,26 +214,77 @@ 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(&self) -> f64 {
@@ -247,7 +292,7 @@ impl Animation {
return self.to;
}
let passed = self.clock.now().saturating_sub(self.start_time);
let passed = self.current_time.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
-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()
}
}
+7 -27
View File
@@ -17,13 +17,9 @@ pub use tty::Tty;
pub mod winit;
pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
pub enum Backend {
Tty(Tty),
Winit(Winit),
Headless(Headless),
}
#[derive(PartialEq, Eq)]
@@ -58,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),
}
}
@@ -66,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(),
}
}
@@ -77,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),
}
}
@@ -90,7 +83,6 @@ 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),
}
}
@@ -98,7 +90,6 @@ impl Backend {
match self {
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
Backend::Headless(_) => CompositorMod::Super,
}
}
@@ -106,7 +97,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.change_vt(vt),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -114,7 +104,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.suspend(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -122,7 +111,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.toggle_debug_tint(),
Backend::Winit(winit) => winit.toggle_debug_tint(),
Backend::Headless(_) => (),
}
}
@@ -130,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),
}
}
@@ -138,7 +125,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -146,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(),
}
}
@@ -158,7 +143,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
Backend::Headless(_) => None,
}
}
@@ -166,7 +150,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -174,7 +157,6 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -182,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(_) => (),
}
}
@@ -209,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")
}
}
}
+251 -238
View File
@@ -18,9 +18,9 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
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};
@@ -50,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;
@@ -64,12 +63,7 @@ use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, 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>>,
@@ -123,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,
@@ -134,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,
@@ -180,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)]
@@ -225,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.
@@ -445,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() {
@@ -462,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,
);
}
}
@@ -608,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(),
@@ -641,7 +629,6 @@ impl Tty {
}
};
let mut added = Vec::new();
let mut removed = Vec::new();
for event in scan_result {
match event {
@@ -650,16 +637,16 @@ impl Tty {
crtc: Some(crtc),
} => {
let connector_name = format_connector_name(&connector);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name, false);
debug!(
"new connector: {} \"{}\"",
&name.connector,
name.format_make_model_serial(),
&output_name.connector,
output_name.format_make_model_serial(),
);
// Assign an id to this crtc.
let id = OutputId::next();
added.push((crtc, CrtcInfo { id, name }));
device.output_ids.insert(crtc, OutputId::next());
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
@@ -680,42 +667,11 @@ impl Tty {
};
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.
//
@@ -807,8 +763,12 @@ 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(),
self.config.borrow().debug.disable_monitor_names,
);
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
@@ -861,6 +821,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();
@@ -879,31 +878,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);
@@ -930,6 +904,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();
@@ -968,7 +959,7 @@ impl Tty {
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
None,
Some(planes),
allocator.clone(),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
@@ -976,7 +967,7 @@ impl Tty {
// 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 {
@@ -994,17 +985,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,
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
Some(device.gbm.clone()),
cursor_plane_gbm,
)
.context("error creating DRM compositor")?
}
@@ -1013,6 +1008,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) {
@@ -1041,8 +1037,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!(
@@ -1060,6 +1054,7 @@ impl Tty {
compositor,
dmabuf_feedback,
gamma_props,
vrr_enabled,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
@@ -1238,17 +1233,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
@@ -1398,35 +1386,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
@@ -1607,13 +1569,17 @@ 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(),
self.config.borrow().debug.disable_monitor_names,
);
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1648,17 +1614,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
@@ -1669,12 +1626,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()),
@@ -1688,6 +1639,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);
}
}
@@ -1745,17 +1700,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;
}
@@ -1821,11 +1773,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;
}
@@ -1847,20 +1796,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 {
@@ -1892,17 +1838,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 {
@@ -1918,9 +1859,16 @@ impl Tty {
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,
self.config.borrow().debug.disable_monitor_names,
);
let config = self
.config
.borrow()
.outputs
.find(&output_name)
.cloned()
@@ -1949,12 +1897,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.
@@ -1971,7 +1931,13 @@ impl Tty {
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,
self.config.borrow().debug.disable_monitor_names,
);
if output_name.matches(target) {
return Some(output_name);
}
@@ -2166,8 +2132,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()
@@ -2175,11 +2142,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<_>>();
@@ -2187,32 +2150,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()?;
@@ -2475,6 +2423,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,
@@ -2520,6 +2486,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!(
"{}-{}",
@@ -2532,7 +2535,17 @@ fn make_output_name(
device: &DrmDevice,
connector: connector::Handle,
connector_name: String,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
return OutputName {
connector: connector_name,
make: None,
model: None,
serial: None,
};
}
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
+4 -2
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};
@@ -216,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(),
);
-2
View File
@@ -64,8 +64,6 @@ 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.
+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 -2
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use super::Start;
@@ -18,7 +18,7 @@ pub enum NiriToScreenshot {
ScreenshotResult(Option<PathBuf>),
}
#[interface(name = "org.gnome.Shell.Screenshot")]
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
impl Screenshot {
async fn screenshot(
&self,
+4 -2
View File
@@ -1,5 +1,5 @@
use zbus::blocking::Connection;
use zbus::object_server::Interface;
use zbus::Interface;
use crate::niri::State;
@@ -83,7 +83,7 @@ impl DBusServers {
dbus.conn_introspect = try_start(introspect);
#[cfg(feature = "xdp-gnome-screencast")]
{
if niri.pipewire.is_some() {
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -95,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");
}
}
+10 -11
View File
@@ -3,9 +3,8 @@ use std::sync::{Arc, Mutex};
use serde::Serialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{fdo, interface};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
@@ -44,7 +43,7 @@ pub struct LogicalMonitor {
properties: HashMap<String, OwnedValue>,
}
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
&self,
@@ -157,8 +156,8 @@ impl DisplayConfig {
Ok((0, monitors, logical_monitors, properties))
}
#[zbus(signal)]
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
@@ -213,16 +212,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″");
}
}
+15 -16
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;
@@ -95,14 +94,14 @@ pub enum ScreenCastToNiri {
session_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,
@@ -137,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(self.id, 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");
@@ -176,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();
}
@@ -265,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) => {
@@ -362,7 +361,7 @@ impl Stream {
}
}
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
+7 -8
View File
@@ -1,8 +1,9 @@
use std::os::fd::{FromRawFd, IntoRawFd};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use smithay::reexports::wayland_server::DisplayHandle;
use zbus::{fdo, interface, zvariant};
use zbus::dbus_interface;
use super::Start;
use crate::niri::ClientState;
@@ -11,14 +12,14 @@ pub struct ServiceChannel {
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(),
));
}
@@ -29,11 +30,9 @@ impl ServiceChannel {
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
restricted: false,
// FIXME: maybe you can get the PID from D-Bus somehow?
credentials_unknown: true,
});
self.display.insert_client(sock2, data).unwrap();
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
}
}
@@ -45,7 +44,7 @@ impl ServiceChannel {
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()?;
+30 -93
View File
@@ -1,6 +1,5 @@
use std::collections::hash_map::Entry;
use niri_ipc::PositionChange;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
@@ -19,8 +18,6 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
use crate::layout::{ActivateWindow, AddWindowTarget};
use crate::niri::{ClientState, State};
use crate::utils::send_scale_transform;
use crate::utils::transaction::Transaction;
@@ -87,23 +84,16 @@ impl CompositorHandler for State {
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,
@@ -114,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))
@@ -176,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);
@@ -253,7 +210,7 @@ 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")]
@@ -284,21 +241,14 @@ impl CompositorHandler for State {
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");
@@ -307,21 +257,8 @@ 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);
self.niri.queue_redraw(&output);
return;
-18
View File
@@ -11,7 +11,6 @@ use smithay::wayland::shell::wlr_layer::{
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;
@@ -61,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
@@ -130,21 +128,6 @@ impl State {
if is_mapped {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let rules = &self.niri.config.borrow().layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let mapped = MappedLayer::new(layer.clone(), rules);
let prev = self
.niri
.mapped_layer_surfaces
.insert(layer.clone(), mapped);
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
@@ -168,7 +151,6 @@ impl State {
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
self.niri.mapped_layer_surfaces.remove(layer);
self.niri.unmapped_layer_surfaces.insert(surface.clone());
}
} else {
+6 -48
View File
@@ -62,8 +62,8 @@ use smithay::{
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_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;
@@ -155,7 +155,7 @@ 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 {
@@ -306,40 +306,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}");
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
// example. On successful drop, additionally activate the target window.
let mut activate_output = true;
if let Some(target) = validated.then_some(target).flatten() {
if let Some(root) = self.niri.root_surface.get(&target) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
activate_output = false;
}
}
}
if activate_output {
// Find the output from cursor coordinates.
//
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
// and if it comes from touch, then what the coordinates are. Need to pass more
// parameters from Smithay I guess.
//
// Assume that hidden pointer means touch DnD.
if !self.niri.pointer_hidden {
// We can't even get the current pointer location because it's locked (we're deep
// in the grab call stack here). So use the last known one.
if let Some(output) = &self.niri.pointer_contents.output {
self.niri.layout.activate_output(output);
}
}
}
fn dropped(&mut self, _seat: Seat<Self>) {
self.niri.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
@@ -405,10 +372,6 @@ impl SessionLockHandler for State {
fn unlock(&mut self) {
self.niri.unlock();
self.niri.activate_monitors(&mut self.backend);
self.niri
.idle_notifier_state
.notify_activity(&self.niri.seat);
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
@@ -447,7 +410,6 @@ impl SecurityContextHandler for State {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
restricted: true,
credentials_unknown: false,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
@@ -675,12 +637,10 @@ impl XdgActivationHandler for State {
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
unmapped.activation_token_data = Some(token_data);
self.niri.activation_state.remove_token(&token);
}
}
self.niri.activation_state.remove_token(&token);
}
}
delegate_xdg_activation!(State);
@@ -702,5 +662,3 @@ delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
delegate_single_pixel_buffer!(State);
+41 -120
View File
@@ -42,7 +42,7 @@ 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::scrolling::ColumnWidth;
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, ResizeEdge};
@@ -209,16 +209,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.
@@ -289,7 +281,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;
};
@@ -298,30 +289,30 @@ impl XdgShellHandler for State {
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
trace!("ignoring popup grab because the session is locked");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
trace!("ignoring popup grab because the screenshot UI is open");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
// This is a grab for a regular window; check that there's no layer surface with a
// higher input priority.
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
trace!("ignoring toplevel popup grab because the overlay layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -334,51 +325,33 @@ impl XdgShellHandler for State {
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
trace!("ignoring toplevel popup grab because the top layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
trace!("ignoring toplevel popup grab because another window has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
trace!("ignoring popup grab because no output is active");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let 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
@@ -387,22 +360,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) {
@@ -492,7 +459,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.
@@ -516,9 +483,6 @@ impl XdgShellHandler for State {
// A configure is required in response to this event regardless if there are pending
// changes.
//
// FIXME: when unfullscreening to floating, this will send an extra configure with
// scrolling layout bounds. We should probably avoid it.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
@@ -530,9 +494,6 @@ impl XdgShellHandler for State {
InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name,
@@ -587,26 +548,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 {
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.
@@ -662,7 +609,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());
@@ -694,22 +641,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);
@@ -821,7 +752,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()");
@@ -883,11 +814,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
@@ -909,26 +836,14 @@ 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 {
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);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
@@ -946,9 +861,6 @@ impl State {
*state = InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name().cloned()),
@@ -1016,8 +928,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)?;
@@ -1027,10 +939,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);
@@ -1050,7 +971,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);
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);
@@ -1095,7 +1016,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(
@@ -1104,7 +1025,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:?}");
}
+42 -320
View File
@@ -36,7 +36,6 @@ use touch_move_grab::TouchMoveGrab;
use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::layout::scrolling::ScrollDirection;
use crate::niri::State;
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
@@ -84,8 +83,11 @@ impl State {
{
let _span = tracy_client::span!("process_input_event");
// Make sure some logic like workspace clean-up has a chance to run before doing actions.
self.niri.advance_animations();
// A bit of a hack, but animation end runs some logic (i.e. workspace clean-up) and it
// doesn't always trigger due to damage, etc. So run it here right before it might prove
// important. Besides, animations affect the input, so it's best to have up-to-date values
// here.
self.niri.layout.advance_animations(get_monotonic_time());
if self.niri.monitors_active {
// Notify the idle-notifier of activity.
@@ -119,7 +121,7 @@ impl State {
.niri
.exit_confirm_dialog
.as_ref()
.is_some_and(|d| d.is_open())
.map_or(false, |d| d.is_open())
&& should_hide_exit_confirm_dialog(&event);
use InputEvent::*;
@@ -521,8 +523,7 @@ impl State {
self.niri.debug_toggle_damage();
}
Action::Spawn(command) => {
let (token, _) = self.niri.activation_state.create_external_token(None);
spawn(command, Some(token.clone()));
spawn(command);
}
Action::DoScreenTransition(delay_ms) => {
self.backend.with_primary_renderer(|renderer| {
@@ -582,8 +583,8 @@ impl State {
self.open_screenshot_ui();
}
Action::ScreenshotWindow => {
let focus = self.niri.layout.focus_with_output();
if let Some((mapped, output)) = focus {
let active = self.niri.layout.active_window();
if let Some((mapped, output)) = active {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) {
warn!("error taking screenshot: {err:?}");
@@ -635,12 +636,22 @@ impl State {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.focus_window(&window);
}
}
Action::FocusWindowPrevious => {
if let Some(window) = self.niri.previously_focused_window.clone() {
self.focus_window(&window);
let active_output = self.niri.layout.active_output().cloned();
self.niri.layout.activate_window(&window);
let new_active = self.niri.layout.active_output().cloned();
#[allow(clippy::collapsible_if)]
if new_active != active_output {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&new_active.unwrap());
}
} else {
self.maybe_warp_cursor_to_focus();
}
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::SwitchLayout(action) => {
@@ -769,42 +780,36 @@ impl State {
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRight => {
self.niri.layout.focus_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnFirst => {
self.niri.layout.focus_column_first();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLast => {
self.niri.layout.focus_column_last();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRightOrFirst => {
self.niri.layout.focus_column_right_or_first();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLeftOrLast => {
self.niri.layout.focus_column_left_or_last();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -821,7 +826,6 @@ impl State {
self.niri.layout.focus_up();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -839,7 +843,6 @@ impl State {
self.niri.layout.focus_down();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -857,7 +860,6 @@ impl State {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -875,7 +877,6 @@ impl State {
self.niri.layout.focus_right();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -883,56 +884,48 @@ impl State {
Action::FocusWindowDown => {
self.niri.layout.focus_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUp => {
self.niri.layout.focus_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDownOrColumnLeft => {
self.niri.layout.focus_down_or_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDownOrColumnRight => {
self.niri.layout.focus_down_or_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUpOrColumnLeft => {
self.niri.layout.focus_up_or_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUpOrColumnRight => {
self.niri.layout.focus_up_or_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceUp => {
self.niri.layout.focus_window_or_workspace_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -989,7 +982,7 @@ impl State {
.niri
.layout
.active_output()
.is_some_and(|active| output.as_ref() == Some(active));
.map_or(false, |active| output.as_ref() == Some(active));
if let Some(output) = output {
self.niri
@@ -1009,8 +1002,8 @@ impl State {
self.niri.layout.move_to_workspace(Some(&window), index);
// If we focused the target window.
let new_focus = self.niri.layout.focus();
if new_focus.is_some_and(|win| win.window == window) {
let new_active_win = self.niri.layout.active_window();
if new_active_win.map_or(false, |(win, _)| win.window == window) {
self.maybe_warp_cursor_to_focus();
}
}
@@ -1061,14 +1054,12 @@ impl State {
Action::FocusWorkspaceDown => {
self.niri.layout.switch_workspace_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspaceUp => {
self.niri.layout.switch_workspace_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -1097,7 +1088,6 @@ impl State {
}
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -1105,8 +1095,6 @@ impl State {
}
Action::FocusWorkspacePrevious => {
self.niri.layout.switch_workspace_previous();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -1120,18 +1108,6 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SetWorkspaceName(name) => {
self.niri.layout.set_workspace_name(name, None);
}
Action::SetWorkspaceNameByRef { name, reference } => {
self.niri.layout.set_workspace_name(name, Some(reference));
}
Action::UnsetWorkspaceName => {
self.niri.layout.unset_workspace_name(None);
}
Action::UnsetWorkSpaceNameByRef(reference) => {
self.niri.layout.unset_workspace_name(Some(reference));
}
Action::ConsumeWindowIntoColumn => {
self.niri.layout.consume_into_column();
// This does not cause immediate focus or window size change, so warping mouse to
@@ -1145,35 +1121,9 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwapWindowRight => {
self.niri
.layout
.swap_window_in_direction(ScrollDirection::Right);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwapWindowLeft => {
self.niri
.layout
.swap_window_in_direction(ScrollDirection::Left);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwitchPresetColumnWidth => {
self.niri.layout.toggle_width();
}
Action::SwitchPresetWindowWidth => {
self.niri.layout.toggle_window_width(None);
}
Action::SwitchPresetWindowWidthById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.toggle_window_width(Some(&window));
}
}
Action::SwitchPresetWindowHeight => {
self.niri.layout.toggle_window_height(None);
}
@@ -1189,20 +1139,6 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::CenterWindow => {
self.niri.layout.center_window(None);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::CenterWindowById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.center_window(Some(&window));
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MaximizeColumn => {
self.niri.layout.toggle_full_width();
}
@@ -1212,7 +1148,6 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorRight => {
@@ -1221,7 +1156,6 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorDown => {
@@ -1230,7 +1164,6 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorUp => {
@@ -1239,25 +1172,6 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::MoveWindowToMonitorLeft => {
@@ -1296,24 +1210,6 @@ impl State {
}
}
}
Action::MoveWindowToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_to_output(None, &output, None);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_to_output(None, &output, None);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_column_to_output(&output);
@@ -1350,37 +1246,9 @@ impl State {
}
}
}
Action::MoveColumnToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::SetColumnWidth(change) => {
self.niri.layout.set_column_width(change);
}
Action::SetWindowWidth(change) => {
self.niri.layout.set_window_width(None, change);
}
Action::SetWindowWidthById { id, change } => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_width(Some(&window), change);
}
}
Action::SetWindowHeight(change) => {
self.niri.layout.set_window_height(None, change);
}
@@ -1438,100 +1306,6 @@ impl State {
}
}
}
Action::MoveWorkspaceToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_workspace_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWorkspaceToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_workspace_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::ToggleWindowFloating => {
self.niri.layout.toggle_window_floating(None);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ToggleWindowFloatingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.toggle_window_floating(Some(&window));
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MoveWindowToFloating => {
self.niri.layout.set_window_floating(None, true);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToFloatingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_floating(Some(&window), true);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MoveWindowToTiling => {
self.niri.layout.set_window_floating(None, false);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToTilingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_floating(Some(&window), false);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::FocusFloating => {
self.niri.layout.focus_floating();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusTiling => {
self.niri.layout.focus_tiling();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwitchFocusBetweenFloatingAndTiling => {
self.niri.layout.switch_focus_floating_tiling();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveFloatingWindowById { id, x, y } => {
let window = if let Some(id) = id {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if window.is_none() {
return;
}
window
} else {
None
};
self.niri
.layout
.move_floating_window(window.as_ref(), x, y, true);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
}
@@ -1785,43 +1559,11 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let button = event.button();
let button_code = event.button_code();
let button = event.button_code();
let button_state = event.state();
// Ignore release events for mouse clicks that triggered a bind.
if self.niri.suppressed_buttons.remove(&button_code) {
return;
}
if ButtonState::Pressed == button_state {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
if self.niri.mods_with_mouse_binds.contains(&modifiers) {
let comp_mod = self.backend.mod_key();
if let Some(bind) = match button {
Some(MouseButton::Left) => Some(Trigger::MouseLeft),
Some(MouseButton::Right) => Some(Trigger::MouseRight),
Some(MouseButton::Middle) => Some(Trigger::MouseMiddle),
Some(MouseButton::Back) => Some(Trigger::MouseBack),
Some(MouseButton::Forward) => Some(Trigger::MouseForward),
_ => None,
}
.and_then(|trigger| {
let config = self.niri.config.borrow();
let bindings = &config.binds;
find_configured_bind(bindings, comp_mod, trigger, mods)
}) {
self.niri.suppressed_buttons.insert(button_code);
self.handle_bind(bind.clone());
return;
};
}
// We received an event for the regular pointer, so show it now.
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
@@ -1830,7 +1572,8 @@ impl State {
let window = mapped.window.clone();
// Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1849,7 +1592,7 @@ impl State {
) {
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
button: event.button_code(),
location,
};
let grab = MoveGrab::new(start_data, window.clone());
@@ -1861,7 +1604,8 @@ impl State {
}
}
// Check if we need to start an interactive resize.
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() {
else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1880,16 +1624,8 @@ impl State {
// FIXME: deduplicate with resize_request in xdg-shell somehow.
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.
@@ -1921,7 +1657,7 @@ impl State {
{
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
button: event.button_code(),
location,
};
let grab = ResizeGrab::new(start_data, window.clone());
@@ -1945,7 +1681,8 @@ impl State {
self.niri.queue_redraw_all();
}
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
if event.button() == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1955,7 +1692,7 @@ impl State {
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
button: event.button_code(),
location,
};
let grab = SpatialMovementGrab::new(start_data, output);
@@ -1975,7 +1712,7 @@ impl State {
self.niri.focus_layer_surface_if_on_demand(layer_under);
}
if let Some(button) = button {
if let Some(button) = event.button() {
let pos = pointer.current_location();
if let Some((output, _)) = self.niri.output_under(pos) {
let output = output.clone();
@@ -2003,7 +1740,7 @@ impl State {
pointer.button(
self,
&ButtonEvent {
button: button_code,
button,
state: button_state,
serial,
time: event.time_msec(),
@@ -3268,20 +3005,6 @@ pub fn mods_with_binds(
rv
}
pub fn mods_with_mouse_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet<Modifiers> {
mods_with_binds(
comp_mod,
binds,
&[
Trigger::MouseLeft,
Trigger::MouseRight,
Trigger::MouseMiddle,
Trigger::MouseBack,
Trigger::MouseForward,
],
)
}
pub fn mods_with_wheel_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet<Modifiers> {
mods_with_binds(
comp_mod,
@@ -3311,7 +3034,6 @@ pub fn mods_with_finger_scroll_binds(comp_mod: CompositorMod, binds: &Binds) ->
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::Clock;
#[test]
fn bindings_suppress_keys() {
@@ -3330,7 +3052,7 @@ mod tests {
let comp_mod = CompositorMod::Super;
let mut suppressed_keys = HashSet::new();
let screenshot_ui = ScreenshotUi::new(Clock::default(), Default::default());
let screenshot_ui = ScreenshotUi::new(Default::default());
let disable_power_key_handling = false;
// The key_code we pick is arbitrary, the only thing
+2 -12
View File
@@ -123,18 +123,8 @@ impl PointerGrab<State> for MoveGrab {
}
}
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
} else {
0x110
};
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
data.niri.layout.toggle_window_floating(Some(&self.window));
}
if !handle.current_pressed().contains(&self.start_data.button) {
// The button that initiated the grab was released.
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
-78
View File
@@ -1,6 +1,3 @@
use std::iter::Peekable;
use std::slice;
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
@@ -26,7 +23,6 @@ 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,
@@ -172,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:?}");
@@ -516,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 {
+3 -52
View File
@@ -16,11 +16,9 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::desktop::layer_map_for_output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use crate::backend::IpcOutputMap;
use crate::layout::workspace::WorkspaceId;
@@ -256,47 +254,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();
@@ -314,9 +271,6 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
// Make sure some logic like workspace clean-up has a chance to run before doing
// actions.
state.niri.advance_animations();
state.do_action(action, false);
let _ = tx.send_blocking(());
});
@@ -409,10 +363,8 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i
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(),
})
}
@@ -523,7 +475,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 });
}
@@ -546,7 +498,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_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()),
}
@@ -593,8 +545,7 @@ 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;
changed |= with_toplevel_role(mapped.toplevel(), |role| {
ipc_win.title != role.title || ipc_win.app_id != role.app_id
-122
View File
@@ -1,122 +0,0 @@
use std::cell::RefCell;
use niri_config::layer_rule::LayerRule;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Rectangle, Scale};
use super::ResolvedLayerRules;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
#[derive(Debug)]
pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,
/// Up-to-date rules.
rules: ResolvedLayerRules,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
}
niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
}
}
impl MappedLayer {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
Self {
surface,
rules,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
}
}
pub fn surface(&self) -> &LayerSurface {
&self.surface
}
pub fn rules(&self) -> &ResolvedLayerRules {
&self.rules
}
/// Recomputes the resolved layer rules and returns whether they changed.
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
if new_rules == self.rules {
return false;
}
self.rules = new_rules;
true
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
geometry: Rectangle<i32, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let geometry = geometry
.to_f64()
.to_physical_precise_round(scale)
.to_logical(scale);
let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(geometry.size.to_f64());
let elem = SolidColorRenderElement::from_buffer(
&buffer,
geometry.loc,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = geometry.loc;
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;
rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
));
}
rv.normal = render_elements_from_surface_tree(
renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
}
rv
}
}
-69
View File
@@ -1,69 +0,0 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::BlockOutFrom;
use smithay::desktop::LayerSurface;
pub mod mapped;
pub use mapped::MappedLayer;
/// Rules fully resolved for a layer-shell surface.
#[derive(Debug, PartialEq)]
pub struct ResolvedLayerRules {
/// Extra opacity to draw this window with.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
}
impl ResolvedLayerRules {
pub const fn empty() -> Self {
Self {
opacity: None,
block_out_from: None,
}
}
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
let _span = tracy_client::span!("ResolvedLayerRules::compute");
let mut resolved = ResolvedLayerRules::empty();
for rule in rules {
let matches = |m: &Match| {
if let Some(at_startup) = m.at_startup {
if at_startup != is_at_startup {
return false;
}
}
surface_matches(surface, m)
};
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}
if rule.excludes.iter().any(matches) {
continue;
}
if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
}
resolved
}
}
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
if let Some(namespace_re) = &m.namespace {
if !namespace_re.0.is_match(surface.namespace()) {
return false;
}
}
true
}
+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
+8 -5
View File
@@ -94,7 +94,7 @@ impl FocusRing {
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,
@@ -178,12 +178,12 @@ 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,
@@ -196,12 +196,15 @@ 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,
+862 -2093
View File
File diff suppressed because it is too large Load Diff
+271 -284
View File
@@ -7,15 +7,15 @@ use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::utils::{Logical, Point, Rectangle};
use super::scrolling::{Column, ColumnWidth, ScrollDirection};
use super::tile::Tile;
use super::workspace::{
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
};
use super::{ActivateWindow, LayoutElement, Options};
use crate::animation::{Animation, Clock};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::input::swipe_tracker::SwipeTracker;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
@@ -45,8 +45,6 @@ pub struct Monitor<W: LayoutElement> {
pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub(super) workspace_switch: Option<WorkspaceSwitch>,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
pub(super) options: Rc<Options>,
}
@@ -68,23 +66,6 @@ pub struct WorkspaceSwitchGesture {
is_touchpad: bool,
}
/// Where to put a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
/// No particular preference.
#[default]
Auto,
/// On this workspace.
Workspace {
/// Id of the target workspace.
id: WorkspaceId,
/// Override where the window will open as a new column.
column_idx: Option<usize>,
},
/// Next to this existing window.
NextTo(&'a W::Id),
}
pub type MonitorRenderElement<R> =
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
@@ -103,20 +84,6 @@ impl WorkspaceSwitch {
}
}
pub fn offset(&mut self, delta: isize) {
match self {
WorkspaceSwitch::Animation(anim) => anim.offset(delta as f64),
WorkspaceSwitch::Gesture(gesture) => {
if delta >= 0 {
gesture.center_idx += delta as usize;
} else {
gesture.center_idx -= (-delta) as usize;
}
gesture.current_idx += delta as f64;
}
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
@@ -127,12 +94,7 @@ impl WorkspaceSwitch {
}
impl<W: LayoutElement> Monitor<W> {
pub fn new(
output: Output,
workspaces: Vec<Workspace<W>>,
clock: Clock,
options: Rc<Options>,
) -> Self {
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
Self {
output_name: output.name(),
output,
@@ -140,7 +102,6 @@ impl<W: LayoutElement> Monitor<W> {
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
clock,
options,
}
}
@@ -165,7 +126,7 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.iter().find(|ws| {
ws.name
.as_ref()
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
@@ -173,7 +134,7 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.iter().position(|ws| {
ws.name
.as_ref()
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
@@ -189,29 +150,6 @@ impl<W: LayoutElement> Monitor<W> {
self.windows().any(|win| win.id() == window)
}
pub fn add_workspace_top(&mut self) {
let ws = Workspace::new(
self.output.clone(),
self.clock.clone(),
self.options.clone(),
);
self.workspaces.insert(0, ws);
self.active_workspace_idx += 1;
if let Some(switch) = &mut self.workspace_switch {
switch.offset(1);
}
}
pub fn add_workspace_bottom(&mut self) {
let ws = Workspace::new(
self.output.clone(),
self.clock.clone(),
self.options.clone(),
);
self.workspaces.push(ws);
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
@@ -229,7 +167,6 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
current_idx,
idx as f64,
0.,
@@ -239,34 +176,65 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_window(
&mut self,
workspace_idx: usize,
window: W,
target: MonitorAddWindowTarget<W>,
activate: ActivateWindow,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
is_floating: bool,
) {
// Currently, everything a workspace sets on a Tile is the same across all workspaces of a
// monitor. So we can use any workspace, not necessarily the exact target workspace.
let tile = self.workspaces[0].make_tile(window);
self.add_tile(tile, target, activate, width, is_full_width, is_floating);
}
pub fn add_column(&mut self, mut workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_column(column, activate);
workspace.add_window(None, window, activate, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
self.add_workspace_bottom();
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if self.options.empty_workspace_above_first && workspace_idx == 0 {
self.add_workspace_top();
workspace_idx += 1;
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace_idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(right_of))
.unwrap();
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window_right_of(right_of, window, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
// Since we're adding window right of something, the workspace isn't empty, and therefore
// cannot be the last one, so we never need to insert a new empty workspace.
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_column(None, column, activate, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
@@ -276,54 +244,27 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_tile(
&mut self,
workspace_idx: usize,
column_idx: Option<usize>,
tile: Tile<W>,
target: MonitorAddWindowTarget<W>,
activate: ActivateWindow,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
is_floating: bool,
) {
let (mut workspace_idx, target) = match target {
MonitorAddWindowTarget::Auto => {
(self.active_workspace_idx, WorkspaceAddWindowTarget::Auto)
}
MonitorAddWindowTarget::Workspace { id, column_idx } => {
let idx = self.workspaces.iter().position(|ws| ws.id() == id).unwrap();
let target = if let Some(column_idx) = column_idx {
WorkspaceAddWindowTarget::NewColumnAt(column_idx)
} else {
WorkspaceAddWindowTarget::Auto
};
(idx, target)
}
MonitorAddWindowTarget::NextTo(win_id) => {
let idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(win_id))
.unwrap();
(idx, WorkspaceAddWindowTarget::NextTo(win_id))
}
};
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile(tile, target, activate, width, is_full_width, is_floating);
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
self.add_workspace_bottom();
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if self.options.empty_workspace_above_first && workspace_idx == 0 {
self.add_workspace_top();
workspace_idx += 1;
}
if activate.map_smart(|| false) {
if activate {
self.activate_workspace(workspace_idx);
}
}
@@ -354,49 +295,40 @@ impl<W: LayoutElement> Monitor<W> {
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
let range_start = if self.options.empty_workspace_above_first {
1
} else {
0
};
for idx in (range_start..self.workspaces.len() - 1).rev() {
for idx in (0..self.workspaces.len() - 1).rev() {
if self.active_workspace_idx == idx {
continue;
}
if !self.workspaces[idx].has_windows_or_name() {
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
}
}
}
}
// Special case handling when empty_workspace_above_first is set and all workspaces
// are empty.
if self.options.empty_workspace_above_first && self.workspaces.len() == 2 {
assert!(!self.workspaces[0].has_windows_or_name());
assert!(!self.workspaces[1].has_windows_or_name());
self.workspaces.remove(1);
self.active_workspace_idx = 0;
pub fn unname_workspace(&mut self, workspace_name: &str) -> bool {
for ws in &mut self.workspaces {
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
{
ws.unname();
return true;
}
}
false
}
pub fn unname_workspace(&mut self, id: WorkspaceId) -> bool {
let Some(ws) = self.workspaces.iter_mut().find(|ws| ws.id() == id) else {
return false;
};
ws.unname();
true
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}
pub fn move_left(&mut self) -> bool {
self.active_workspace().move_left()
}
pub fn move_right(&mut self) -> bool {
self.active_workspace().move_right()
pub fn move_right(&mut self) {
self.active_workspace().move_right();
}
pub fn move_column_to_first(&mut self) {
@@ -416,23 +348,40 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_down_or_to_workspace_down(&mut self) {
if !self.active_workspace().move_down() {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let column = &mut workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.move_to_workspace_down();
} else {
workspace.move_down();
}
}
pub fn move_up_or_to_workspace_up(&mut self) {
if !self.active_workspace().move_up() {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.move_to_workspace_up();
} else {
workspace.move_up();
}
}
pub fn focus_left(&mut self) -> bool {
self.active_workspace().focus_left()
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
}
pub fn focus_right(&mut self) -> bool {
self.active_workspace().focus_right()
pub fn focus_right(&mut self) {
self.active_workspace().focus_right();
}
pub fn focus_column_first(&mut self) {
@@ -451,39 +400,98 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().focus_column_left_or_last();
}
pub fn focus_down(&mut self) -> bool {
self.active_workspace().focus_down()
pub fn focus_down(&mut self) {
self.active_workspace().focus_down();
}
pub fn focus_up(&mut self) -> bool {
self.active_workspace().focus_up()
pub fn focus_up(&mut self) {
self.active_workspace().focus_up();
}
pub fn focus_down_or_left(&mut self) {
self.active_workspace().focus_down_or_left();
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_down();
}
}
}
pub fn focus_down_or_right(&mut self) {
self.active_workspace().focus_down_or_right();
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_right();
} else {
workspace.focus_down();
}
}
}
pub fn focus_up_or_left(&mut self) {
self.active_workspace().focus_up_or_left();
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_up();
}
}
}
pub fn focus_up_or_right(&mut self) {
self.active_workspace().focus_up_or_right();
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_right();
} else {
workspace.focus_up();
}
}
}
pub fn focus_window_or_workspace_down(&mut self) {
if !self.active_workspace().focus_down() {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_down();
} else {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.switch_workspace_down();
} else {
workspace.focus_down();
}
}
}
pub fn focus_window_or_workspace_up(&mut self) {
if !self.active_workspace().focus_up() {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.switch_workspace_up();
} else {
workspace.focus_up();
}
}
}
@@ -494,23 +502,26 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let workspace = &mut self.workspaces[source_workspace_idx];
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
if workspace.columns.is_empty() {
return;
};
}
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
ActivateWindow::Yes,
let column = &workspace.columns[workspace.active_column_idx];
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
removed.width,
removed.is_full_width,
removed.is_floating,
);
}
@@ -521,71 +532,75 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let workspace = &mut self.workspaces[source_workspace_idx];
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
if workspace.columns.is_empty() {
return;
};
}
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
ActivateWindow::Yes,
let column = &workspace.columns[workspace.active_column_idx];
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
removed.width,
removed.is_full_width,
removed.is_floating,
);
}
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
let source_workspace_idx = if let Some(window) = window {
let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window {
self.workspaces
.iter()
.position(|ws| ws.has_window(window))
.enumerate()
.find_map(|(ws_idx, ws)| {
ws.columns.iter().enumerate().find_map(|(col_idx, col)| {
col.tiles
.iter()
.position(|tile| tile.window().id() == window)
.map(|tile_idx| (ws_idx, col_idx, tile_idx))
})
})
.unwrap()
} else {
self.active_workspace_idx
let ws_idx = self.active_workspace_idx;
let ws = &self.workspaces[ws_idx];
if ws.columns.is_empty() {
return;
}
let col_idx = ws.active_column_idx;
let tile_idx = ws.columns[col_idx].active_tile_idx;
(ws_idx, col_idx, tile_idx)
};
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let activate = window.map_or(true, |win| {
self.active_window().map(|win| win.id()) == Some(win)
});
let activate = if activate {
ActivateWindow::Yes
} else {
ActivateWindow::No
};
let workspace = &mut self.workspaces[source_workspace_idx];
let transaction = Transaction::new();
let removed = if let Some(window) = window {
workspace.remove_tile(window, transaction)
} else if let Some(removed) = workspace.remove_active_tile(transaction) {
removed
} else {
return;
};
let column = &workspace.columns[col_idx];
let activate = source_workspace_idx == self.active_workspace_idx
&& col_idx == workspace.active_column_idx
&& tile_idx == column.active_tile_idx;
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
self.add_window(
new_idx,
removed.tile.into_window(),
activate,
removed.width,
removed.is_full_width,
removed.is_floating,
);
if self.workspace_switch.is_none() {
@@ -602,15 +617,11 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.floating_is_active() {
self.move_to_workspace_up();
if workspace.columns.is_empty() {
return;
}
let Some(column) = workspace.remove_active_column() else {
return;
};
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
}
@@ -623,15 +634,11 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.floating_is_active() {
self.move_to_workspace_down();
if workspace.columns.is_empty() {
return;
}
let Some(column) = workspace.remove_active_column() else {
return;
};
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
}
@@ -644,15 +651,11 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.floating_is_active() {
self.move_to_workspace(None, idx);
if workspace.columns.is_empty() {
return;
}
let Some(column) = workspace.remove_active_column() else {
return;
};
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
}
@@ -702,24 +705,23 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().expel_from_column();
}
pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) {
self.active_workspace().swap_window_in_direction(direction);
}
pub fn center_column(&mut self) {
self.active_workspace().center_column();
}
pub fn active_window(&self) -> Option<&W> {
self.active_workspace_ref().active_window()
pub fn focus(&self) -> Option<&W> {
let workspace = &self.workspaces[self.active_workspace_idx];
if !workspace.has_windows() {
return None;
}
let column = &workspace.columns[workspace.active_column_idx];
Some(column.tiles[column.active_tile_idx].window())
}
pub fn is_active_fullscreen(&self) -> bool {
self.active_workspace_ref().is_active_fullscreen()
}
pub fn advance_animations(&mut self) {
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
@@ -727,7 +729,7 @@ impl<W: LayoutElement> Monitor<W> {
}
for ws in &mut self.workspaces {
ws.advance_animations();
ws.advance_animations(current_time);
}
}
@@ -776,21 +778,21 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn update_config(&mut self, options: Rc<Options>) {
if self.options.empty_workspace_above_first != options.empty_workspace_above_first
&& self.workspaces.len() > 1
{
if options.empty_workspace_above_first {
self.add_workspace_top();
} else if self.workspace_switch.is_none() && self.active_workspace_idx != 0 {
self.workspaces.remove(0);
self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
}
}
for ws in &mut self.workspaces {
ws.update_config(options.clone());
}
if self.options.struts != options.struts {
let scale = self.output.current_scale();
let transform = self.output.current_transform();
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(scale, transform, view_size, working_area);
}
}
self.options = options;
}
@@ -807,7 +809,7 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_workspace_down(&mut self) {
let mut new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
return;
}
@@ -816,12 +818,8 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && self.active_workspace_idx == 0 {
self.add_workspace_top();
new_idx += 1;
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
@@ -833,7 +831,7 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_workspace_up(&mut self) {
let mut new_idx = self.active_workspace_idx.saturating_sub(1);
let new_idx = self.active_workspace_idx.saturating_sub(1);
if new_idx == self.active_workspace_idx {
return;
}
@@ -842,12 +840,8 @@ impl<W: LayoutElement> Monitor<W> {
if self.active_workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && new_idx == 0 {
self.add_workspace_top();
new_idx += 1;
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
@@ -870,7 +864,7 @@ impl<W: LayoutElement> Monitor<W> {
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = offset * size.h;
let clip_rect = Rectangle::new(Point::from((0., -offset)), size);
let clip_rect = Rectangle::from_loc_and_size((0., -offset), size);
rect = rect.intersection(clip_rect)?;
}
@@ -929,7 +923,7 @@ impl<W: LayoutElement> Monitor<W> {
let size = output_size(&self.output);
let (ws, bounds) = self
.workspaces_with_render_positions()
.map(|(ws, offset)| (ws, Rectangle::new(offset, size)))
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
Some((ws, bounds.loc))
}
@@ -962,8 +956,7 @@ impl<W: LayoutElement> Monitor<W> {
&'a self,
renderer: &'a mut R,
target: RenderTarget,
focus_ring: bool,
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a {
) -> impl Iterator<Item = MonitorRenderElement<R>> + '_ {
let _span = tracy_client::span!("Monitor::render_elements");
let scale = self.output.current_scale().fractional_scale();
@@ -982,20 +975,15 @@ impl<W: LayoutElement> Monitor<W> {
//
// FIXME: use proper bounds after fixing the Crop element.
let crop_bounds = if self.workspace_switch.is_some() {
Rectangle::new(
Point::from((-i32::MAX / 2, 0)),
Size::from((i32::MAX, height)),
)
Rectangle::from_loc_and_size((-i32::MAX / 2, 0), (i32::MAX, height))
} else {
Rectangle::new(
Point::from((-i32::MAX / 2, -i32::MAX / 2)),
Size::from((i32::MAX, i32::MAX)),
)
Rectangle::from_loc_and_size((-i32::MAX / 2, -i32::MAX / 2), (i32::MAX, i32::MAX))
};
self.workspaces_with_render_positions()
.flat_map(move |(ws, offset)| {
ws.render_elements(renderer, target, focus_ring)
ws.render_elements(renderer, target)
.into_iter()
.filter_map(move |elem| {
CropRenderElement::from_element(elem, scale, crop_bounds)
})
@@ -1074,7 +1062,7 @@ impl<W: LayoutElement> Monitor<W> {
return false;
};
if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) {
if is_touchpad.map_or(false, |x| gesture.is_touchpad != x) {
return false;
}
@@ -1111,7 +1099,6 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
gesture.current_idx,
new_idx as f64,
velocity,
+5 -2
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -40,7 +41,9 @@ impl OpenAnimation {
}
}
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()
@@ -72,7 +75,7 @@ impl OpenAnimation {
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
let mut area = Rectangle::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);
File diff suppressed because it is too large Load Diff
+41 -142
View File
@@ -1,4 +1,5 @@
use std::rc::Rc;
use std::time::Duration;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
@@ -9,10 +10,10 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac,
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};
@@ -48,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>,
@@ -91,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>,
}
@@ -136,29 +110,18 @@ struct MoveAnimation {
}
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 is_fullscreen = window.is_fullscreen();
Self {
window,
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
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,
@@ -166,28 +129,12 @@ impl<W: LayoutElement> Tile<W> {
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;
@@ -200,8 +147,6 @@ impl<W: LayoutElement> Tile<W> {
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
self.fullscreen_backdrop.resize(view_size);
}
pub fn update_shaders(&mut self) {
@@ -210,7 +155,10 @@ impl<W: LayoutElement> Tile<W> {
}
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 = if let Some(resize) = self.resize_animation.take() {
@@ -232,13 +180,7 @@ impl<W: LayoutElement> Tile<W> {
let change = self.window.size().to_f64().to_point() - size_from.to_point();
let change = f64::max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(
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,
@@ -266,25 +208,29 @@ 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;
}
@@ -318,7 +264,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_window_size(),
is_active,
!draw_border_with_background,
Rectangle::new(
Rectangle::from_loc_and_size(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
),
@@ -370,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.,
@@ -398,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,
@@ -417,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,
@@ -438,6 +383,10 @@ 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
}
@@ -462,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.
@@ -492,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;
}
@@ -532,15 +462,6 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
let size = self.window.expected_size();
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
size
}
fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
@@ -564,8 +485,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);
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
return size;
}
@@ -590,7 +511,7 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_size(self.tile_size());
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
activation_region.contains(point)
}
@@ -646,9 +567,10 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn request_fullscreen(&mut self) {
self.window
.request_fullscreen(self.view_size.to_i32_round());
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(&self) -> Size<f64, Logical> {
@@ -711,7 +633,7 @@ impl<W: LayoutElement> Tile<W> {
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::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);
@@ -810,7 +732,7 @@ impl<W: LayoutElement> Tile<W> {
.window
.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();
@@ -852,12 +774,12 @@ 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,
@@ -994,27 +916,4 @@ impl<W: LayoutElement> Tile<W> {
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
self.unmap_snapshot.take()
}
pub fn options(&self) -> &Rc<Options> {
&self.options
}
#[cfg(test)]
pub fn view_size(&self) -> Size<f64, Logical> {
self.view_size
}
#[cfg(test)]
pub fn verify_invariants(&self) {
use approx::assert_abs_diff_eq;
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
let scale = self.scale;
let size = self.tile_size();
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
}
}
+3667 -1120
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;
+10 -3
View File
@@ -11,6 +11,7 @@ use std::{env, mem};
use clap::Parser;
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
@@ -163,6 +164,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
})
.unwrap_or_default();
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
@@ -176,7 +184,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
false,
)
.unwrap();
@@ -237,10 +244,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.
+145 -427
View File
File diff suppressed because it is too large Load Diff
+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) => (),
+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);
+22 -29
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;
@@ -41,7 +41,7 @@ use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Scale, Size, Transform};
use zbus::object_server::SignalEmitter;
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State;
@@ -54,14 +54,12 @@ const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
pub struct PipeWire {
_context: Context,
pub core: Core,
pub token: RegistrationToken,
to_niri: calloop::channel::Sender<PwToNiri>,
}
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw(CastTarget),
FatalError,
}
pub struct Cast {
@@ -136,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);
@@ -167,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);
@@ -175,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,
})
}
@@ -194,7 +188,7 @@ impl PipeWire {
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");
@@ -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();
@@ -844,9 +834,12 @@ 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;
@@ -1030,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))
}
+12 -12
View File
@@ -6,7 +6,7 @@ use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::damage::ExtraDamage;
use super::renderer::{AsGlesFrame as _, NiriRenderer};
@@ -117,21 +117,21 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
let bottom_left = corner_radius.bottom_left as f64;
[
Rectangle::new(geo.loc, Size::from((top_left, top_left))),
Rectangle::new(
Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)),
Size::from((top_right, top_right)),
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
Rectangle::from_loc_and_size(
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
(top_right, top_right),
),
Rectangle::new(
Point::from((
Rectangle::from_loc_and_size(
(
geo.loc.x + geo.size.w - bottom_right,
geo.loc.y + geo.size.h - bottom_right,
)),
Size::from((bottom_right, bottom_right)),
),
(bottom_right, bottom_right),
),
Rectangle::new(
Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)),
Size::from((bottom_left, bottom_left)),
Rectangle::from_loc_and_size(
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
(bottom_left, bottom_left),
),
]
}
+1 -1
View File
@@ -54,7 +54,7 @@ impl Element for ExtraDamage {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(Size::from((1., 1.)))
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
+2 -7
View File
@@ -113,11 +113,6 @@ impl<E> SplitElements<E> {
popups.extend(normal);
popups
}
pub fn extend(&mut self, other: SplitElements<E>) {
self.popups.extend(other.popups);
self.normal.extend(other.normal);
}
}
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
@@ -216,7 +211,7 @@ pub fn render_and_download(
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mapping = renderer
.copy_framebuffer(Rectangle::from_size(buffer_size), fourcc)
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
@@ -300,7 +295,7 @@ fn render_elements(
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let transform = transform.invert();
let output_rect = Rectangle::from_size(transform.transform_size(size));
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
+2 -2
View File
@@ -46,7 +46,7 @@ impl AsGlesRenderer for GlesRenderer {
}
}
impl AsGlesRenderer for TtyRenderer<'_> {
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
@@ -66,7 +66,7 @@ impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
}
}
impl<'frame> AsGlesFrame<'frame> for TtyFrame<'_, 'frame> {
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
+1 -1
View File
@@ -43,7 +43,7 @@ impl ResizeRenderElement {
let tex_next_geo_scaled = tex_next_geo.to_f64().upscale(scale_next);
let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled).to_i32_up();
let area = Rectangle::new(
let area = Rectangle::from_loc_and_size(
area.loc + combined_geo.loc.to_logical(scale),
combined_geo.size.to_logical(scale),
);
+4 -4
View File
@@ -193,7 +193,7 @@ impl ShaderRenderElement {
program,
id: Id::new(),
commit_counter: CommitCounter::default(),
area: Rectangle::from_size(size),
area: Rectangle::from_loc_and_size((0., 0.), size),
opaque_regions: opaque_regions.unwrap_or_default(),
scale,
alpha,
@@ -255,7 +255,7 @@ impl Element for ShaderRenderElement {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(Size::from((1., 1.)))
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -314,7 +314,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
[
rect.loc.x as f32,
rect.loc.y as f32,
@@ -334,7 +334,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
(0..6).flat_map(move |_| {
[
+4 -3
View File
@@ -85,7 +85,7 @@ impl SolidColorRenderElement {
alpha: f32,
kind: Kind,
) -> Self {
let geo = Rectangle::new(location.into(), buffer.size());
let geo = Rectangle::from_loc_and_size(location, buffer.size());
let color = buffer.color * alpha;
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
}
@@ -125,7 +125,7 @@ impl Element for SolidColorRenderElement {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(Size::from((1., 1.)))
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -134,7 +134,8 @@ impl Element for SolidColorRenderElement {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if self.color.is_opaque() {
let rect = Rectangle::from_size(self.geometry.size).to_physical_precise_down(scale);
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
.to_physical_precise_down(scale);
OpaqueRegions::from_slice(&[rect])
} else {
OpaqueRegions::default()
+4 -2
View File
@@ -157,7 +157,7 @@ impl<T: Texture> Element for TextureRenderElement<T> {
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
let logical_geo = Rectangle::new(self.location, self.logical_size());
let logical_geo = Rectangle::from_loc_and_size(self.location, self.logical_size());
logical_geo.to_physical_precise_round(scale)
}
@@ -174,7 +174,9 @@ impl<T: Texture> Element for TextureRenderElement<T> {
&self.buffer.logical_size(),
)
})
.unwrap_or_else(|| Rectangle::from_size(self.buffer.texture.size()).to_f64())
.unwrap_or_else(|| {
Rectangle::from_loc_and_size((0, 0), self.buffer.texture.size()).to_f64()
})
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
-536
View File
@@ -1,536 +0,0 @@
use std::cmp::min;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::Write as _;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::{env, fmt};
use calloop::EventLoop;
use calloop_wayland_source::WaylandSource;
use single_pixel_buffer::v1::client::wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1;
use smithay::reexports::wayland_protocols::wp::single_pixel_buffer;
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter;
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_surface::{self, XdgSurface};
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_toplevel::{self, XdgToplevel};
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_wm_base::{self, XdgWmBase};
use wayland_backend::client::Backend;
use wayland_client::globals::Global;
use wayland_client::protocol::wl_buffer::{self, WlBuffer};
use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_compositor::WlCompositor;
use wayland_client::protocol::wl_display::WlDisplay;
use wayland_client::protocol::wl_output::{self, WlOutput};
use wayland_client::protocol::wl_registry::{self, WlRegistry};
use wayland_client::protocol::wl_surface::{self, WlSurface};
use wayland_client::{Connection, Dispatch, Proxy as _, QueueHandle};
use crate::utils::id::IdCounter;
pub struct Client {
pub id: ClientId,
pub event_loop: EventLoop<'static, State>,
pub connection: Connection,
pub qh: QueueHandle<State>,
pub display: WlDisplay,
pub state: State,
}
pub struct State {
pub qh: QueueHandle<State>,
pub globals: Vec<Global>,
pub outputs: HashMap<WlOutput, String>,
pub compositor: Option<WlCompositor>,
pub xdg_wm_base: Option<XdgWmBase>,
pub spbm: Option<WpSinglePixelBufferManagerV1>,
pub viewporter: Option<WpViewporter>,
pub windows: Vec<Window>,
}
pub struct Window {
pub qh: QueueHandle<State>,
pub spbm: WpSinglePixelBufferManagerV1,
pub surface: WlSurface,
pub xdg_surface: XdgSurface,
pub xdg_toplevel: XdgToplevel,
pub viewport: WpViewport,
pub pending_configure: Configure,
pub configures_received: Vec<(u32, Configure)>,
pub close_requsted: bool,
pub configures_looked_at: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Configure {
pub size: (i32, i32),
pub bounds: Option<(i32, i32)>,
pub states: Vec<xdg_toplevel::State>,
}
#[derive(Default)]
pub struct SyncData {
pub done: AtomicBool,
}
static CLIENT_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClientId(u64);
impl ClientId {
fn next() -> ClientId {
ClientId(CLIENT_ID_COUNTER.next())
}
}
impl fmt::Display for Configure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "size: {} × {}, ", self.size.0, self.size.1)?;
if let Some(bounds) = self.bounds {
write!(f, "bounds: {} × {}, ", bounds.0, bounds.1)?;
} else {
write!(f, "bounds: none, ")?;
}
write!(f, "states: {:?}", self.states)?;
Ok(())
}
}
fn connect(socket_name: &OsStr) -> Connection {
let mut socket_path = PathBuf::from(env::var_os("XDG_RUNTIME_DIR").unwrap());
socket_path.push(socket_name);
let stream = UnixStream::connect(socket_path).unwrap();
let backend = Backend::connect(stream).unwrap();
Connection::from_backend(backend)
}
impl Client {
pub fn new(socket_name: &OsStr) -> Self {
let id = ClientId::next();
let event_loop = EventLoop::try_new().unwrap();
let connection = connect(socket_name);
let queue = connection.new_event_queue();
let qh = queue.handle();
WaylandSource::new(connection.clone(), queue)
.insert(event_loop.handle())
.unwrap();
let display = connection.display();
let _registry = display.get_registry(&qh, ());
connection.flush().unwrap();
let state = State {
qh: qh.clone(),
globals: Vec::new(),
outputs: HashMap::new(),
compositor: None,
xdg_wm_base: None,
spbm: None,
viewporter: None,
windows: Vec::new(),
};
Self {
id,
event_loop,
connection,
qh,
display,
state,
}
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
}
pub fn send_sync(&self) -> Arc<SyncData> {
let data = Arc::new(SyncData::default());
self.display.sync(&self.qh, data.clone());
self.connection.flush().unwrap();
data
}
pub fn create_window(&mut self) -> &mut Window {
self.state.create_window()
}
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
self.state.window(surface)
}
pub fn output(&mut self, name: &str) -> WlOutput {
self.state
.outputs
.iter()
.find(|(_, v)| *v == name)
.unwrap()
.0
.clone()
}
}
impl State {
pub fn create_window(&mut self) -> &mut Window {
let compositor = self.compositor.as_ref().unwrap();
let xdg_wm_base = self.xdg_wm_base.as_ref().unwrap();
let viewporter = self.viewporter.as_ref().unwrap();
let surface = compositor.create_surface(&self.qh, ());
let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &self.qh, ());
let xdg_toplevel = xdg_surface.get_toplevel(&self.qh, ());
let viewport = viewporter.get_viewport(&surface, &self.qh, ());
let window = Window {
qh: self.qh.clone(),
spbm: self.spbm.clone().unwrap(),
surface,
xdg_surface,
xdg_toplevel,
viewport,
pending_configure: Configure::default(),
configures_received: Vec::new(),
close_requsted: false,
configures_looked_at: 0,
};
self.windows.push(window);
self.windows.last_mut().unwrap()
}
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
self.windows
.iter_mut()
.find(|w| w.surface == *surface)
.unwrap()
}
}
impl Window {
pub fn commit(&self) {
self.surface.commit();
}
pub fn ack_last(&self) {
let serial = self.configures_received.last().unwrap().0;
self.xdg_surface.ack_configure(serial);
}
pub fn ack_last_and_commit(&self) {
self.ack_last();
self.commit();
}
pub fn attach_new_buffer(&self) {
let buffer = self.spbm.create_u32_rgba_buffer(0, 0, 0, 0, &self.qh, ());
self.surface.attach(Some(&buffer), 0, 0);
}
pub fn set_size(&self, w: u16, h: u16) {
self.viewport.set_destination(i32::from(w), i32::from(h));
}
pub fn set_fullscreen(&self, output: Option<&WlOutput>) {
self.xdg_toplevel.set_fullscreen(output);
}
pub fn unset_fullscreen(&self) {
self.xdg_toplevel.unset_fullscreen();
}
pub fn set_parent(&self, parent: Option<&XdgToplevel>) {
self.xdg_toplevel.set_parent(parent);
}
pub fn set_title(&self, title: &str) {
self.xdg_toplevel.set_title(title.to_owned());
}
pub fn recent_configures(&mut self) -> impl Iterator<Item = &Configure> {
let start = self.configures_looked_at;
self.configures_looked_at = self.configures_received.len();
self.configures_received[start..].iter().map(|(_, c)| c)
}
pub fn format_recent_configures(&mut self) -> String {
let mut buf = String::new();
for configure in self.recent_configures() {
if !buf.is_empty() {
buf.push('\n');
}
write!(buf, "{configure}").unwrap();
}
buf
}
}
impl Dispatch<WlCallback, Arc<SyncData>> for State {
fn event(
_state: &mut Self,
_proxy: &WlCallback,
event: <WlCallback as wayland_client::Proxy>::Event,
data: &Arc<SyncData>,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_callback::Event::Done { .. } => data.done.store(true, Ordering::Relaxed),
_ => unreachable!(),
}
}
}
impl Dispatch<WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &WlRegistry,
event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global {
name,
interface,
version,
} => {
if interface == WlCompositor::interface().name {
let version = min(version, WlCompositor::interface().version);
state.compositor = Some(registry.bind(name, version, qh, ()));
} else if interface == XdgWmBase::interface().name {
let version = min(version, XdgWmBase::interface().version);
state.xdg_wm_base = Some(registry.bind(name, version, qh, ()));
} else if interface == WpSinglePixelBufferManagerV1::interface().name {
let version = min(version, WpSinglePixelBufferManagerV1::interface().version);
state.spbm = Some(registry.bind(name, version, qh, ()));
} else if interface == WpViewporter::interface().name {
let version = min(version, WpViewporter::interface().version);
state.viewporter = Some(registry.bind(name, version, qh, ()));
} else if interface == WlOutput::interface().name {
let version = min(version, WlOutput::interface().version);
let output = registry.bind(name, version, qh, ());
state.outputs.insert(output, String::new());
}
let global = Global {
name,
interface,
version,
};
state.globals.push(global);
}
wl_registry::Event::GlobalRemove { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
output: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_output::Event::Geometry { .. } => (),
wl_output::Event::Mode { .. } => (),
wl_output::Event::Done => (),
wl_output::Event::Scale { .. } => (),
wl_output::Event::Name { name } => {
*state.outputs.get_mut(output).unwrap() = name;
}
wl_output::Event::Description { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlCompositor, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlCompositor,
_event: <WlCompositor as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<XdgWmBase, ()> for State {
fn event(
_state: &mut Self,
xdg_wm_base: &XdgWmBase,
event: <XdgWmBase as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
xdg_wm_base::Event::Ping { serial } => {
xdg_wm_base.pong(serial);
}
_ => unreachable!(),
}
}
}
impl Dispatch<WlSurface, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlSurface,
event: <WlSurface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_surface::Event::Enter { .. } => (),
wl_surface::Event::Leave { .. } => (),
wl_surface::Event::PreferredBufferScale { .. } => (),
wl_surface::Event::PreferredBufferTransform { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<XdgSurface, ()> for State {
fn event(
state: &mut Self,
xdg_surface: &XdgSurface,
event: <XdgSurface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
xdg_surface::Event::Configure { serial } => {
let window = state
.windows
.iter_mut()
.find(|w| w.xdg_surface == *xdg_surface)
.unwrap();
let configure = window.pending_configure.clone();
window.configures_received.push((serial, configure));
}
_ => unreachable!(),
}
}
}
impl Dispatch<XdgToplevel, ()> for State {
fn event(
state: &mut Self,
xdg_toplevel: &XdgToplevel,
event: <XdgToplevel as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
let window = state
.windows
.iter_mut()
.find(|w| w.xdg_toplevel == *xdg_toplevel)
.unwrap();
match event {
xdg_toplevel::Event::Configure {
width,
height,
states,
} => {
let configure = &mut window.pending_configure;
configure.size = (width, height);
configure.states = states
.chunks_exact(4)
.flat_map(TryInto::<[u8; 4]>::try_into)
.map(u32::from_ne_bytes)
.flat_map(xdg_toplevel::State::try_from)
.collect();
}
xdg_toplevel::Event::Close => {
window.close_requsted = true;
}
xdg_toplevel::Event::ConfigureBounds { width, height } => {
window.pending_configure.bounds = Some((width, height));
}
xdg_toplevel::Event::WmCapabilities { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlBuffer, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlBuffer,
event: <WlBuffer as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_buffer::Event::Release => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WpSinglePixelBufferManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpSinglePixelBufferManagerV1,
_event: <WpSinglePixelBufferManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<WpViewporter, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpViewporter,
_event: <WpViewporter as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<WpViewport, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpViewport,
_event: <WpViewport as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
-139
View File
@@ -1,139 +0,0 @@
use std::os::fd::AsFd as _;
use std::sync::atomic::Ordering;
use std::time::Duration;
use calloop::generic::Generic;
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
use niri_config::Config;
use smithay::output::Output;
use super::client::{Client, ClientId};
use super::server::Server;
use crate::niri::Niri;
pub struct Fixture {
pub event_loop: EventLoop<'static, State>,
pub handle: LoopHandle<'static, State>,
pub state: State,
}
pub struct State {
pub server: Server,
pub clients: Vec<Client>,
}
impl Fixture {
pub fn new() -> Self {
Self::with_config(Config::default())
}
pub fn with_config(config: Config) -> Self {
let event_loop = EventLoop::try_new().unwrap();
let handle = event_loop.handle();
let server = Server::new(config);
let fd = server.event_loop.as_fd().try_clone_to_owned().unwrap();
let source = Generic::new(fd, Interest::READ, Mode::Level);
handle
.insert_source(source, |_, _, state: &mut State| {
state.server.dispatch();
Ok(PostAction::Continue)
})
.unwrap();
let state = State {
server,
clients: Vec::new(),
};
Self {
event_loop,
handle,
state,
}
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
}
pub fn niri_state(&mut self) -> &mut crate::niri::State {
&mut self.state.server.state
}
pub fn niri(&mut self) -> &mut Niri {
&mut self.niri_state().niri
}
pub fn niri_output(&self, n: u8) -> Output {
let niri = &self.state.server.state.niri;
let idx = usize::from(n - 1);
let output = niri.global_space.outputs().nth(idx).unwrap();
output.clone()
}
pub fn niri_focus_output(&mut self, n: u8) {
let niri = &mut self.state.server.state.niri;
let idx = usize::from(n - 1);
let output = niri.global_space.outputs().nth(idx).unwrap();
niri.layout.focus_output(output);
}
pub fn add_output(&mut self, n: u8, size: (u16, u16)) {
let state = self.niri_state();
let niri = &mut state.niri;
state.backend.headless().add_output(niri, n, size);
}
pub fn add_client(&mut self) -> ClientId {
let client = Client::new(&self.state.server.state.niri.socket_name);
let id = client.id;
let fd = client.event_loop.as_fd().try_clone_to_owned().unwrap();
let source = Generic::new(fd, Interest::READ, Mode::Level);
self.handle
.insert_source(source, move |_, _, state: &mut State| {
state.client(id).dispatch();
Ok(PostAction::Continue)
})
.unwrap();
self.state.clients.push(client);
self.roundtrip(id);
id
}
pub fn client(&mut self, id: ClientId) -> &mut Client {
self.state.client(id)
}
pub fn roundtrip(&mut self, id: ClientId) {
let client = self.state.client(id);
let data = client.send_sync();
while !data.done.load(Ordering::Relaxed) {
self.dispatch();
}
}
/// Rountrip twice in a row.
///
/// For some reason, when running tests on many threads at once, a single roundtrip is
/// sometimes not sufficient to get the configure events to the client.
///
/// I suspect that this is because these configure events are sent from the niri loop callback,
/// so they arrive after the sync done event and don't get processed in that client dispatch
/// cycle. I'm not sure why this would be dependent on multithreading. But if this is indeed
/// the issue, then a double roundtrip fixes it.
pub fn double_roundtrip(&mut self, id: ClientId) {
self.roundtrip(id);
self.roundtrip(id);
}
}
impl State {
pub fn client(&mut self, id: ClientId) -> &mut Client {
self.clients.iter_mut().find(|c| c.id == id).unwrap()
}
}
-856
View File
@@ -1,856 +0,0 @@
use client::ClientId;
use insta::assert_snapshot;
use niri_config::Config;
use niri_ipc::SizeChange;
use smithay::utils::Point;
use wayland_client::protocol::wl_surface::WlSurface;
use super::*;
// Sets up a fixture with two outputs and 100×100 window.
fn set_up() -> (Fixture, ClientId, WlSurface) {
let mut f = Fixture::new();
f.add_output(1, (1920, 1080));
f.add_output(2, (1280, 720));
let id = f.add_client();
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.commit();
f.roundtrip(id);
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(100, 100);
window.ack_last_and_commit();
f.double_roundtrip(id);
(f, id, surface)
}
#[test]
fn unfocus_preserves_current_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.roundtrip(id);
// Change window size while it's floating.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Focus a different output which should drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request 200 × 200 because that's the current window size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
);
// Change window size again.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// Focus the first output which should add back the Activated state.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request 300 × 300 because that's the current window size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_different_size() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the Activated state change configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
f.niri().layout.toggle_window_floating(None);
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size, 500 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Focus a different output which should drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size since the window hasn't committed yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Ack but don't commit yet.
let window = f.client(id).window(&surface);
window.ack_last();
f.roundtrip(id);
// Add the activated state.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the new size since the window hasn't committed yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Commit but with some different size.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the current window size rather than keep requesting 500 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
);
}
#[test]
fn set_window_width_uses_current_height() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different on both axes.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a width change.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should use the current window height (200), rather than the initial window height (100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn set_window_height_uses_current_width() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different on both axes.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a width change.
f.niri()
.layout
.set_window_height(None, SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should use the current window width (200), rather than the initial window width (100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 500, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_same_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a size change to the same size.
f.niri().layout.set_column_width(SizeChange::SetFixed(200));
f.double_roundtrip(id);
// This needn't request anything because we're already that size; the size in the current
// server state matches the requested size.
//
// FIXME: However, currently it will request the size anyway because the code checks the
// current server state, and the last size niri requested of the window was 100×100 (even if
// the window already acked and committed in response).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_different_then_same() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to any configure from the floating change.
let window = f.client(id).window(&surface);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Before the window has a chance to respond, request a size change to the same, new size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
// And also drop the Activated state to have some pending change.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should keep requesting the new size (500 × 100) since the window has not responded to
// it yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Commit in response to the size change request.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// And also add the Activated state to have some pending change.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the current window size (300 × 300) since the window has committed in
// response to the size change.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn restore_floating_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit in response to the floating configure.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Change back to tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a tiling size configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Resize as requested.
let window = f.client(id).window(&surface);
let (_, configure) = window.configures_received.last().unwrap();
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
window.ack_last_and_commit();
f.roundtrip(id);
// Change back to floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a configure restoring out previous 200 × 200 size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn moving_across_workspaces_doesnt_cancel_resize() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit in response to the floating configure.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
);
// Move to a different workspace before the window has a chance to respond. This will remove it
// from one floating layout and add into a different one, potentially causing a size request.
f.niri().layout.move_to_workspace_down();
// Drop the Activated state to force a configure.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size again (500 × 200) since the window hasn't responded to it.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: []"
);
// Respond to the resize with a different size.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// Focus, adding Activated, and move to workspace down, causing removing and adding to a
// floating layout.
f.niri_focus_output(1);
f.niri().layout.move_to_workspace_down();
f.double_roundtrip(id);
// This should request the current size (300 × 300) since the window responded to the change.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn moving_to_floating_doesnt_cancel_resize() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size (500 ×).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Before the window has a chance to respond, make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should keep requesting the new size (500 ×).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 1048, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn interactive_move_unfullscreen_to_floating_restores_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
niri.layout.set_fullscreen(&window, true);
f.double_roundtrip(id);
// This should request a fullscreen size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 1920 × 1080, bounds: 1888 × 1048, states: [Activated, Fullscreen]"
);
// Start an interactive move which causes an unfullscreen into floating.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
niri.layout
.interactive_move_begin(window.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This should request the stored floating size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_during_interactive_move_propagates_to_floating() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Start an interactive move.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
niri.layout
.interactive_move_begin(window_id.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window_id,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This shouldn't request any new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Change size while we're being interactively moved.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.commit();
f.double_roundtrip(id);
// This shouldn't request any new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// End the interactive move, placing the window into floating.
f.niri().layout.interactive_move_end(&window_id);
f.double_roundtrip(id);
// This should keep the new 300 × 300 size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_in_steps() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the floating bounds state change configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
// Request a size change to a different size in two steps.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.niri()
.layout
.set_window_height(None, SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the full new size (500 × 500) once.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 500, bounds: 1920 × 1080, states: [Activated]"
);
let window = f.client(id).window(&surface);
let serial = window.configures_received.last().unwrap().0;
// Request a size change now that the previous one is pending-but-not-acked.
f.niri().layout.set_column_width(SizeChange::SetFixed(600));
// Drop Activated to work around resize throttling.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size (600 × 500) once.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 600 × 500, bounds: 1920 × 1080, states: []"
);
// Commit in response to the previous configure.
let window = f.client(id).window(&surface);
window.xdg_surface.ack_configure(serial);
window.set_size(500, 500);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Request a height change now that the first one is committed-to, but the second isn't.
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
f.niri()
.layout
.set_window_height(Some(&window), SizeChange::SetFixed(600));
// Add Activated to work around resize throttling.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the latest sizes (600 × 600).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 600 × 600, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn state_change_doesnt_break_use_window_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the bounds change that comes with toggling floating.
f.client(id).window(&surface).ack_last_and_commit();
f.roundtrip(id);
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size (500 × 100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
let window = f.client(id).window(&surface);
let serial = window.configures_received.last().unwrap().0;
// Request a state change by dropping Activated.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size (500 × 100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Commit in response to the previous configure with a different size.
let window = f.client(id).window(&surface);
window.xdg_surface.ack_configure(serial);
window.set_size(300, 300);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Request a height change now that the first one is committed-to, but the second isn't.
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
f.niri()
.layout
.set_window_height(Some(&window), SizeChange::SetFixed(600));
// Add Activated state to force a configure.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should already request the current width (300 × 600) rather than the pending previous
// width (500 × 600) from the state change configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 600, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn interactive_move_restores_floating_size_when_set_to_floating() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit to make niri remember it.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Change back to tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a tiled size configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Resize as requested.
let window = f.client(id).window(&surface);
let (_, configure) = window.configures_received.last().unwrap();
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
window.ack_last_and_commit();
f.roundtrip(id);
// Start an interactive move.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
niri.layout
.interactive_move_begin(window_id.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window_id,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This shouldn't request any new size because interactive move targets tiling.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1920 × 1080, states: [Activated]"
);
// Change interactive move to target floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should restore the floating window size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
// End the interactive move, placing the window into floating.
f.niri().layout.interactive_move_end(&window_id);
f.double_roundtrip(id);
// This should keep the floating window size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
}
#[test]
fn floating_doesnt_store_fullscreen_size() {
let mut f = Fixture::new();
f.add_output(1, (1920, 1080));
f.add_output(2, (1280, 720));
// Open a window fullscreen.
let id = f.add_client();
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.set_fullscreen(None);
window.commit();
f.roundtrip(id);
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(1920, 1080);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should request 0 × 0 to unfullscreen.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 0 × 0, bounds: 1920 × 1080, states: [Activated]"
);
// Without committing, make it tiling again. We never committed while floating, so there's no
// floating size to remember.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should request the tiled size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 1920 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Commit in response.
let window = f.client(id).window(&surface);
window.set_size(100, 100);
window.ack_last_and_commit();
f.roundtrip(id);
// Make the window floating again.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This shouldn't request any size change, particularly not the fullscreen size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 100 × 100, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn floating_respects_non_fixed_min_max_rule() {
let config = r##"
window-rule {
min-width 200
max-width 300
}
"##;
let config = Config::parse("test.kdl", config).unwrap();
let mut f = Fixture::with_config(config);
f.add_output(1, (1920, 1080));
f.add_output(2, (1280, 720));
let id = f.add_client();
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.commit();
f.roundtrip(id);
// Open with smaller width than min.
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(100, 100);
window.ack_last_and_commit();
f.double_roundtrip(id);
// Commit to the Activated state configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should clamp to min-width and request 200 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Commit with a bigger width than max.
let window = f.client(id).window(&surface);
window.set_size(400, 100);
window.ack_last_and_commit();
f.roundtrip(id);
// Make it tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
f.client(id).window(&surface).ack_last_and_commit();
f.roundtrip(id);
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should clamp to max-width and request 300 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 100, bounds: 1920 × 1080, states: [Activated]"
);
}
-8
View File
@@ -1,8 +0,0 @@
use fixture::Fixture;
mod client;
mod fixture;
mod server;
mod floating;
mod window_opening;
-37
View File
@@ -1,37 +0,0 @@
use std::time::Duration;
use calloop::EventLoop;
use niri_config::Config;
use smithay::reexports::wayland_server::Display;
use crate::niri::State;
pub struct Server {
pub event_loop: EventLoop<'static, State>,
pub state: State,
}
impl Server {
pub fn new(config: Config) -> Self {
let event_loop = EventLoop::try_new().unwrap();
let handle = event_loop.handle();
let display = Display::new().unwrap();
let state = State::new(
config,
handle.clone(),
event_loop.get_signal(),
display,
true,
)
.unwrap();
Self { event_loop, state }
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
self.state.refresh_and_flush_clients();
}
}
@@ -1,13 +0,0 @@
---
source: src/tests/window_opening.rs
description: "config:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 616 × 688, bounds: 1248 × 688, states: [Activated]
@@ -1,13 +0,0 @@
---
source: src/tests/window_opening.rs
description: "set parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -1,13 +0,0 @@
---
source: src/tests/window_opening.rs
description: "set parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -1,13 +0,0 @@
---
source: src/tests/window_opening.rs
description: "set parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 0 × 0, bounds: 1280 × 720, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -1,13 +0,0 @@
---
source: src/tests/window_opening.rs
description: "set parent: B2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-2
final workspace: 0 (ws-2)
initial configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
post-map configures:
size: 1 × 1, bounds: 1920 × 1080, states: []
@@ -1,17 +0,0 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -1,17 +0,0 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -1,17 +0,0 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 0 × 0, bounds: 1280 × 720, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -1,17 +0,0 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: B2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-2
final workspace: 0 (ws-2)
initial configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1920 × 1080, bounds: 1888 × 1048, states: [Fullscreen]
unfullscreen configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
@@ -1,17 +0,0 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 616 × 688, bounds: 1248 × 688, states: [Activated]

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