Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49fc6117fd | |||
| 165a6d1ee8 | |||
| fdb6d85fc7 | |||
| 188c5300f7 | |||
| a4b5539baa | |||
| 6f1a2c5f0e | |||
| f717ae030f | |||
| f3696081d1 | |||
| 4b60cbe537 | |||
| f9f43d826a | |||
| 3d49db3870 | |||
| 9bd6c2cadd | |||
| c5253968b4 | |||
| 9a6f31012d | |||
| 4294948cf1 | |||
| cd5ac3e5e0 | |||
| 38191826cb | |||
| 0200670d9e | |||
| 90366886b2 | |||
| 56654034e9 | |||
| 1f07cffa9f | |||
| cb3a06cd54 | |||
| f115f2e5e7 | |||
| 2e07282977 | |||
| cba0454c94 | |||
| 5f6f131b24 | |||
| adb5b3cd2c | |||
| 0650e7b640 | |||
| dd1c3bcb9f | |||
| e5d463e15b | |||
| 26100096e8 | |||
| a85b922919 | |||
| 7d2b620ce9 | |||
| a48f2645d9 | |||
| 83e839762f | |||
| 4c1196f45b | |||
| 8ed0da44d9 | |||
| 91be662ac6 | |||
| 4438aefc8d | |||
| 6c4dfd7772 | |||
| 1ad422f0db | |||
| 8fd9fb73f2 | |||
| 8d83fbae67 | |||
| 414729dce5 | |||
| dbe79b7873 | |||
| 9438f59e2b | |||
| 719255ac35 | |||
| 8a51935224 | |||
| 8d583fe854 | |||
| 74d2b18603 | |||
| fad02316f1 | |||
| 47385c2ecd | |||
| e430d3ab2b | |||
| e472b5b0f1 | |||
| efb169416d | |||
| 3a3a97ec2a | |||
| e9c182a13c | |||
| cfe059c303 | |||
| bd7c748a4f | |||
| 68bb942d21 | |||
| 04c422e43f | |||
| d09fa2709c | |||
| 2c3315aebb | |||
| 5a45088061 | |||
| 404d6dccc4 | |||
| 084f2cb193 | |||
| 25c88b542f | |||
| 6fc50a1fb8 | |||
| 5e11b96f12 | |||
| 849d26d646 | |||
| 9e5716a9db | |||
| f4ebbc8017 | |||
| ce9dd33213 | |||
| 10995ec62c | |||
| c814c656c5 | |||
| 82d4c7569e | |||
| 4f0db78248 | |||
| 3e250cdc12 | |||
| a1b0bd6d1c | |||
| 892470afd3 | |||
| f1cb02cfab | |||
| 5dc4e83ba7 | |||
| 2b58e03d30 | |||
| d4b4407236 | |||
| 26ff5f4bf1 | |||
| d7905e6b74 | |||
| 71d7fa9a61 | |||
| 707f08559c |
@@ -1,12 +0,0 @@
|
||||
# LFS configuration for images from the wiki
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Exclude LFS-tracked files from the tarball
|
||||
/docs/wiki/img/ export-ignore
|
||||
|
||||
# exclude .gitattributes itself from the tarball
|
||||
.gitattributes export-ignore
|
||||
|
||||
# tip: can be tested using
|
||||
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
|
||||
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Report a bug or a crash
|
||||
title: ''
|
||||
labels: bug
|
||||
type: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -20,9 +20,6 @@ jobs:
|
||||
|
||||
name: test
|
||||
runs-on: ubuntu-24.04
|
||||
# FIXME: remove once it's available in runs-on.
|
||||
# This is necessary for libwayland-server v1.23.
|
||||
container: ubuntu:26.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -31,8 +28,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y ${{ env.DEPS_APT }}
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -114,9 +111,6 @@ jobs:
|
||||
|
||||
name: randomized and slow tests
|
||||
runs-on: ubuntu-24.04
|
||||
# FIXME: remove once it's available in runs-on.
|
||||
# This is necessary for libwayland-server v1.23.
|
||||
container: ubuntu:26.04
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -133,8 +127,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y ${{ env.DEPS_APT }}
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -236,7 +230,7 @@ jobs:
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
container: fedora:42
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -28,9 +28,12 @@ jobs:
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
# Fail if a match is found (exit code 0)
|
||||
grep --recursive 'Since: next release' docs/wiki && exit 1
|
||||
|
||||
# Fail if grep failed (exit code 2)
|
||||
status=$?
|
||||
if [ $status -ne 1 ]; then exit $status; fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.11.0"
|
||||
version = "26.4.0"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -16,29 +16,25 @@ rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.102"
|
||||
bitflags = "2.11.0"
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
insta = "1.46.3"
|
||||
bitflags = "2.11.1"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
insta = "1.47.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
# 0.3.20 filters out all ANSI codes to "fix a security issue" while also breaking
|
||||
# everyone who relied on them for color output, with no fallback available.
|
||||
# https://github.com/tokio-rs/tracing/issues/3378
|
||||
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
rev = "dce4d34e7421559b661af9c519904f4b24346148"
|
||||
# path = "../smithay"
|
||||
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
# version = "0.1.0"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
rev = "dce4d34e7421559b661af9c519904f4b24346148"
|
||||
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
[package]
|
||||
@@ -55,8 +51,8 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.24.0", optional = true }
|
||||
accesskit_unix = { version = "0.21.0", optional = true }
|
||||
accesskit = { version = "0.24", optional = true }
|
||||
accesskit_unix = { version = "0.22", optional = true }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.5.0"
|
||||
@@ -66,37 +62,37 @@ bitflags.workspace = true
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.66"
|
||||
clap_complete_nushell = "4.5.10"
|
||||
clap_complete = "4.6.2"
|
||||
clap_complete_nushell = "4.6.0"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.1"
|
||||
fastrand = "2.3.0"
|
||||
fastrand = "2.4.1"
|
||||
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.32.1"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
input = { version = "0.10.0", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.182"
|
||||
libc = "0.2.185"
|
||||
libdisplay-info = "0.3.0"
|
||||
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.11.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.11.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.1.0"
|
||||
niri-config = { version = "26.4.0", path = "niri-config" }
|
||||
niri-ipc = { version = "26.4.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.3.0"
|
||||
pango = { version = "0.21.5", features = ["v1_44"] }
|
||||
pangocairo = "0.21.5"
|
||||
pipewire = { version = "0.9.2", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.18.1"
|
||||
profiling = "1.0.17"
|
||||
sd-notify = "0.4.5"
|
||||
sd-notify = "0.5.0"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
wayland-backend = "0.3.14"
|
||||
wayland-scanner = "0.31.9"
|
||||
wayland-server = { version = "0.31.12", features = ["libwayland_1_23"] }
|
||||
wayland-backend = "0.3.15"
|
||||
wayland-scanner = "0.31.10"
|
||||
wayland-server = "0.31.13"
|
||||
xcursor = "0.3.10"
|
||||
zbus = { version = "5.13.2", optional = true }
|
||||
|
||||
@@ -122,14 +118,14 @@ features = [
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.1"
|
||||
insta.workspace = true
|
||||
proptest = "1.10.0"
|
||||
proptest = "1.11.0"
|
||||
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
|
||||
rayon = "1.11.0"
|
||||
wayland-client = "0.31.13"
|
||||
rayon = "1.12.0"
|
||||
wayland-client = "0.31.14"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3.32"
|
||||
pkg-config = "0.3.33"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
@@ -150,6 +146,7 @@ dinit = []
|
||||
|
||||
[lints.clippy]
|
||||
new_without_default = "allow"
|
||||
collapsible_match = "allow"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -165,7 +162,7 @@ insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "25.11"
|
||||
version = "26.04"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://niri-wm.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://niri-wm.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/niri-wm/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||
<img width="1280" height="720" alt="niri with a few windows open" src="https://github.com/user-attachments/assets/dea5909e-1859-4aaa-9d88-d37f9663e00b" />
|
||||
|
||||
## About
|
||||
|
||||
@@ -39,6 +39,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
- Group windows into [tabs](https://niri-wm.github.io/niri/Tabs.html)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
|
||||
- [Background blur](https://niri-wm.github.io/niri/Window-Effects.html) for windows and layer-shell surfaces
|
||||
- [Animations](https://github.com/niri-wm/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/niri-wm/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- Live-reloading config
|
||||
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
|
||||
@@ -47,7 +48,10 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
|
||||
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||
|
||||
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
|
||||
Also check out these videos that showcase a lot of the niri functionality:
|
||||
|
||||
- [Niri Is My New Favorite Wayland Compositor](https://www.youtube.com/watch?v=DeYx2exm04M) by Brodie Robertson
|
||||
- [How Is niri This Good? Live Demo + Config](https://www.youtube.com/watch?v=7XmD5UyyhZQ) by Nick Janetakis
|
||||
|
||||
## Status
|
||||
|
||||
@@ -56,7 +60,7 @@ 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://niri-wm.github.io/niri/Getting-Started.html) page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
Grab a desktop shell like [DankMaterialShell] or [Noctalia] (or build a more traditional setup): niri by itself is not a complete desktop environment.
|
||||
Also check out [awesome-niri], a list of niri-related links and projects.
|
||||
|
||||
Here are some points you may have questions about:
|
||||
@@ -109,8 +113,8 @@ Here are some other projects which implement a similar workflow:
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/).
|
||||
- [Paneru] and [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -124,8 +128,9 @@ We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
|
||||
[awesome-niri]: https://github.com/niri-wm/awesome-niri
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[Paneru]: https://github.com/karinushka/paneru
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[DankMaterialShell]: https://danklinux.com/
|
||||
[Noctalia]: https://noctalia.dev/
|
||||
|
||||
@@ -88,6 +88,7 @@ nav:
|
||||
- Window Effects: Window-Effects.md
|
||||
- Packaging niri: Packaging-niri.md
|
||||
- Integrating niri: Integrating-niri.md
|
||||
- Security Model: Security-Model.md
|
||||
- Accessibility: Accessibility.md
|
||||
- Name and Logo: Name-and-Logo.md
|
||||
- FAQ: FAQ.md
|
||||
@@ -111,6 +112,7 @@ nav:
|
||||
- Design Principles: Development:-Design-Principles.md
|
||||
- Developing niri: Development:-Developing-niri.md
|
||||
- Documenting niri: Development:-Documenting-niri.md
|
||||
- Releasing niri: Development:-Releasing-niri.md
|
||||
- Fractional Layout: Development:-Fractional-Layout.md
|
||||
- Redraw Loop: Development:-Redraw-Loop.md
|
||||
- Animation Timing: Development:-Animation-Timing.md
|
||||
|
||||
@@ -33,6 +33,8 @@ JetBrains IDEs can run directly on Wayland, but it's not the default.
|
||||
|
||||
For JetBrainsRuntime > 17, you can set the flag `-Dawt.toolkit.name=WLToolkit` inside of `help -> edit custom vm options -> add`.
|
||||
|
||||
If the settings window fails to load under Wayland, and the UI becomes unresponsive afterwards, also set the flag `-Dsun.awt.wl.WindowDecorationStyle=builtin` in the custom vm options. This gives the settings window a titlebar, but it at least makes the IDE functional.
|
||||
|
||||
### WezTerm
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -107,6 +107,8 @@ debug {
|
||||
|
||||
### `force-disable-connectors-on-resume`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
|
||||
This causes a modeset/screen blank on all outputs.
|
||||
|
||||
@@ -322,27 +324,6 @@ debug {
|
||||
}
|
||||
```
|
||||
|
||||
### `keep-max-bpc-unchanged`
|
||||
|
||||
<sup>Since: 25.08</sup>
|
||||
|
||||
When connecting monitors, niri sets their max bpc to 8 in order to reduce display bandwidth and to potentially allow more monitors to be connected at once.
|
||||
Restricting bpc to 8 is not a problem since we don't support HDR or color management yet and can't really make use of higher bpc.
|
||||
|
||||
Apparently, setting max bpc to 8 breaks some displays driven by AMDGPU.
|
||||
If this happens to you, set this debug flag, which will prevent niri from changing max bpc.
|
||||
AMDGPU bug report: https://gitlab.freedesktop.org/drm/amd/-/issues/4487.
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
This setting is deprecated and does nothing: niri no longer sets max bpc.
|
||||
The old niri behavior with this setting enabled matches the new behavior.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
keep-max-bpc-unchanged
|
||||
}
|
||||
```
|
||||
|
||||
### Key Bindings
|
||||
|
||||
These are not debug options, but rather key bindings.
|
||||
|
||||
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
|
||||
Included config files can in turn include more files.
|
||||
All included files are watched for changes, and the config live-reloads when any of them change.
|
||||
|
||||
You can include by filename or path.
|
||||
|
||||
* Relative to the current file: `other.kdl` or `./other.kdl`
|
||||
* By absolute path: `/path/to/file.kdl`
|
||||
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
|
||||
|
||||
Includes work only at the top level of the config:
|
||||
|
||||
```kdl,must-fail
|
||||
@@ -116,7 +122,7 @@ window-rule {
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
@@ -89,6 +89,8 @@ input {
|
||||
tablet {
|
||||
// off
|
||||
map-to-output "eDP-1"
|
||||
// map-to-focused-output
|
||||
// map-to-focused-window
|
||||
// left-handed
|
||||
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
|
||||
}
|
||||
@@ -281,6 +283,16 @@ Valid output names are the same as the ones used for output configuration.
|
||||
|
||||
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
|
||||
|
||||
Settings specific to `tablet`:
|
||||
|
||||
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
|
||||
|
||||
- `map-to-focused-window`: <sup>Since: next release</sup> will map the tablet to the focused window's geometry, takes precedence over `map-to-focused-output` and `map-to-output`.
|
||||
Falls back to those when no window is focused (for example, in the overview).
|
||||
|
||||
When the tablet is also mapped to a specific output via `map-to-output`, the `map-to-focused-window` flag will map the tablet to the active window on that output.
|
||||
If the tablet isn't mapped to any specific output, it will map the tablet to the current focused window regardless of where it is.
|
||||
|
||||
### General Settings
|
||||
|
||||
These settings are not specific to a particular input device.
|
||||
|
||||
@@ -382,7 +382,7 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
|
||||
|
||||
```kdl
|
||||
|
||||
@@ -91,7 +91,7 @@ layer-rule {
|
||||
|
||||
#### `layer`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Matches surfaces on this layer-shell layer.
|
||||
Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
|
||||
@@ -230,7 +230,7 @@ layer-rule {
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override the background effect options for this surface.
|
||||
|
||||
@@ -256,7 +256,7 @@ layer-rule {
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ layout {
|
||||
### `preset-column-widths`
|
||||
|
||||
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (not bound by default) to toggle in reverse.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
|
||||
|
||||
`proportion` sets the width as a fraction of the output width, taking gaps into account.
|
||||
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
|
||||
@@ -229,7 +229,7 @@ layout {
|
||||
|
||||
<sup>Since: 0.1.9</sup>
|
||||
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
|
||||
|
||||
`proportion` sets the height as a fraction of the output height, taking gaps into account.
|
||||
|
||||
@@ -331,13 +331,14 @@ config-notification {
|
||||
|
||||
### `blur`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Blur configuration that affects all background blur.
|
||||
|
||||
See the [window effects page](./Window-Effects.md) for an overview of background effects.
|
||||
|
||||
```kdl
|
||||
// These are the default values:
|
||||
blur {
|
||||
// off
|
||||
passes 3
|
||||
@@ -362,7 +363,7 @@ blur {
|
||||
|
||||
#### `passes` and `offset`
|
||||
|
||||
`passes` contols the number of downsample/upsample passes for dual kawase blur.
|
||||
`passes` controls the number of downsample/upsample passes for dual kawase blur.
|
||||
More passes produce a larger, smoother blur, but cost more GPU resources.
|
||||
|
||||
`offset` is the pixel offset multiplier for each pass.
|
||||
|
||||
@@ -15,6 +15,7 @@ output "eDP-1" {
|
||||
variable-refresh-rate // on-demand=true
|
||||
focus-at-startup
|
||||
backdrop-color "#001100"
|
||||
// max-bpc 8
|
||||
|
||||
hot-corners {
|
||||
// off
|
||||
@@ -279,6 +280,27 @@ output "HDMI-A-1" {
|
||||
}
|
||||
```
|
||||
|
||||
### `max-bpc`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
|
||||
Set the maximum bits per channel (BPC) for this output.
|
||||
|
||||
You *do not* need to set this option normally.
|
||||
It influences the encoding of the display signal on the wire and *is not* directly related to the color bitness or framebuffer format.
|
||||
|
||||
Setting `max-bpc` to a low value may help if you hit a bandwidth issue (can't set a monitor configuration that works on other compositor).
|
||||
Otherwise, you're advised to leave it unset (keeping a default, usually high value) and let the GPU driver figure things out automatically.
|
||||
|
||||
Valid values are `6`, `8`, `10`, `12`, `14`, `16`.
|
||||
|
||||
```kdl
|
||||
// Set 8 max-bpc on HDMI-A-1 to lower the bandwidth.
|
||||
output "HDMI-A-1" {
|
||||
max-bpc 8
|
||||
}
|
||||
```
|
||||
|
||||
### `hot-corners`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
@@ -930,7 +930,7 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override the background effect options for this window.
|
||||
|
||||
@@ -944,6 +944,9 @@ See the [window effects page](./Window-Effects.md) for an overview of background
|
||||
```kdl
|
||||
// Make floating windows use the regular blur (if enabled),
|
||||
// while tiled windows keep using the efficient xray blur.
|
||||
//
|
||||
// Warning: non-xray blur is currently experimental and has known limitations.
|
||||
// In particular, it doesn't work during window opening and closing animations.
|
||||
window-rule {
|
||||
match is-floating=true
|
||||
|
||||
@@ -955,7 +958,7 @@ window-rule {
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this window's pop-ups (menus and tooltips).
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
This is a checklist of things to release a new niri version.
|
||||
|
||||
We'll use `26.04` as the example new version.
|
||||
When making a patch release, append the patch number like `26.04.1`.
|
||||
|
||||
## Prepare the release notes
|
||||
|
||||
Plan for a few days of work, this usually takes a while.
|
||||
|
||||
During this process, also check:
|
||||
|
||||
- that all additions are marked with "next release" on the wiki,
|
||||
- if anything needs updating in `README.md`.
|
||||
|
||||
## Bump version
|
||||
|
||||
We use `year.month.patch` versioning.
|
||||
If the month contains a leading zero, drop it from the crate version (Cargo requirement).
|
||||
|
||||
You can use the command from [cargo-edit](https://github.com/killercup/cargo-edit):
|
||||
|
||||
```
|
||||
cargo set-version 26.4.0
|
||||
```
|
||||
|
||||
Then, manually update version in:
|
||||
|
||||
- `[package.metadata.generate-rpm]` in Cargo.toml
|
||||
- Dependency example in `niri-ipc/README.md`
|
||||
- Dependency example in `niri-ipc/src/lib.rs`
|
||||
|
||||
Do a full text search for the old version to make sure there are no other places.
|
||||
|
||||
## Replace all "Since: next release" mentions
|
||||
|
||||
Do a full text search for `next release`, replace everything with the new version number.
|
||||
|
||||
## Build, test, push, and have the CI run
|
||||
|
||||
Run all tests:
|
||||
|
||||
```
|
||||
RUN_SLOW_TESTS=1 cargo test --release --all
|
||||
```
|
||||
|
||||
- Run `cargo package -p niri-ipc` and make sure it succeeds.
|
||||
- Make sure the CI passes.
|
||||
- Make sure the niri-git COPR build passes.
|
||||
|
||||
## Trigger the "Prepare release" workflow on GitHub Actions
|
||||
|
||||
Set the "Public version" input to a version like `26.04`.
|
||||
|
||||
This workflow will:
|
||||
|
||||
- do some pre-release checks like grepping the wiki for "next version",
|
||||
- make a vendored dependency archive,
|
||||
- build and test niri with that dependency archive,
|
||||
- draft a new GitHub release with the archive attached.
|
||||
It will NOT override an existing draft release with the same name so the release notes are safe.
|
||||
|
||||
Make sure it succeeds and grab the vendored dependency archive that it produces.
|
||||
|
||||
## Update the niri COPR spec, update licenses in .spec.rpkg
|
||||
|
||||
You can grab the previous spec from [the last build](https://copr.fedorainfracloud.org/coprs/yalter/niri/builds/) in the COPR.
|
||||
|
||||
- Update version global to `26.04`.
|
||||
- Update commit global to the commit hash corresponding to the release commit.
|
||||
You can use `git rev-parse HEAD`.
|
||||
- Reset the `Release:` number to 1 if it was higher.
|
||||
|
||||
To run a test build, you can download the vendored dependency archive from the last step.
|
||||
Comment/uncomment `Source:` and `%autosetup` lines accordingly.
|
||||
|
||||
Download the source files:
|
||||
|
||||
```
|
||||
spectool -g niri.spec
|
||||
```
|
||||
|
||||
Build RPMs:
|
||||
|
||||
```
|
||||
fedpkg --release 44 mockbuild
|
||||
```
|
||||
|
||||
During the build, it will print the list of licenses.
|
||||
Update it in both the COPR spec and in `niri.spec.rpkg` accordingly.
|
||||
|
||||
If you had to update `niri.spec.rpkg` and therefore make another commit to the niri repo, make sure to update the commit hash in the COPR spec again.
|
||||
|
||||
Revert any temporary changes that you did to the COPR spec for local testing.
|
||||
|
||||
## Create and push the release git tag
|
||||
|
||||
The tag starts with a `v`:
|
||||
|
||||
```
|
||||
git tag -am "v26.04 release" v26.04
|
||||
git push origin v26.04
|
||||
```
|
||||
|
||||
While you can let GitHub create the tag automatically upon creating the release, this is not recommended.
|
||||
GitHub creates a *lightweight* tag, but we want an annotated tag that plays better with various tooling.
|
||||
|
||||
## Publish the release on GitHub
|
||||
|
||||
- Either upload the vendored dependencies file to your draft release with the release notes, or move the release notes to the GitHub-created release (the difference is that it's attributed to github-actions).
|
||||
- Set the tag to `v26.04`.
|
||||
- Set the release title to `v26.04`.
|
||||
- Check "Create a discussion for this release".
|
||||
|
||||
## Publish the niri-ipc crate
|
||||
|
||||
```
|
||||
cargo publish -p niri-ipc
|
||||
```
|
||||
|
||||
## Kick off the COPR build
|
||||
|
||||
Upload on the web or:
|
||||
|
||||
```
|
||||
copr-cli build niri niri.spec
|
||||
```
|
||||
|
||||
## Announce the release
|
||||
|
||||
Chat rooms, social media, etc.
|
||||
|
||||
## Update wayland.app protocol data
|
||||
|
||||
- Install [wlprobe](https://github.com/PolyMeilex/wlprobe).
|
||||
- Clone https://github.com/vially/wayland-explorer.
|
||||
- Generate data:
|
||||
|
||||
```
|
||||
wlprobe > ./src/data/compositors/niri.json
|
||||
```
|
||||
|
||||
- Manually add `"version": "26.04"`, then clean up the diff from unrelated changes, for example:
|
||||
- The number of `wl_output`s will change depending on how many monitors you have connected.
|
||||
- The number of `wp_drm_lease_device_v1` will change depending on your number of GPUs.
|
||||
- `org_kde_kwin_server_decoration_manager` and `zxdg_decoration_manager_v1` will only appear with `prefer-no-csd`.
|
||||
- Create a pull request.
|
||||
@@ -40,6 +40,20 @@ hotkey-overlay {
|
||||
}
|
||||
```
|
||||
|
||||
### How to fix lag on external monitors connected to a hybrid GPU laptop?
|
||||
|
||||
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
|
||||
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
|
||||
|
||||
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
|
||||
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
|
||||
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
|
||||
|
||||
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
|
||||
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
|
||||
|
||||
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
|
||||
|
||||
### How to run X11 apps like Steam or Discord?
|
||||
|
||||
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
|
||||
@@ -66,10 +80,8 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
|
||||
|
||||
### Can I enable blur behind semitransparent windows?
|
||||
|
||||
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/54).
|
||||
|
||||
There's also [a PR](https://github.com/niri-wm/niri/pull/1634) adding blur to niri which you can build and run manually.
|
||||
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
|
||||
<sup>Since: 26.04</sup> Yes.
|
||||
See the [window effects](./Window-Effects.md) wiki page.
|
||||
|
||||
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
You can maximize an individual window via `maximize-window-to-edges`.
|
||||
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
|
||||
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
|
||||
You will still see your bar, but not struts, gaps, or borders.
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ sudo dnf install niri dms
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
|
||||
Arch Linux:
|
||||
```
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
|
||||
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
@@ -147,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
|
||||
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
|
||||
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
|
||||
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
|
||||
@@ -13,7 +13,7 @@ Keep in mind that we update the default config in new releases, so if you have a
|
||||
|
||||
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
|
||||
|
||||
<sup>Since: next release</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
|
||||
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
|
||||
|
||||
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
|
||||
@@ -63,3 +63,7 @@ Alternatively, some desktop environments and shells work with niri, and can give
|
||||
- Many [XFCE](https://www.xfce.org/) components work on Wayland, including niri. See [their wiki](https://wiki.xfce.org/releng/wayland_roadmap#component_specific_status) for details.
|
||||
- There are complete desktop shells based on Quickshell that support niri, for example [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell) and [Noctalia](https://github.com/noctalia-dev/noctalia-shell).
|
||||
- You can run a [COSMIC](https://system76.com/cosmic/) session with niri using [cosmic-ext-extra-sessions](https://github.com/Drakulix/cosmic-ext-extra-sessions).
|
||||
|
||||
### Security model
|
||||
|
||||
See the [Security Model](./Security-Model.md) page for an overview of niri's security model.
|
||||
|
||||
@@ -53,12 +53,12 @@ It shows up as "niri Dynamic Cast Target" in the screencast window dialog.
|
||||
|
||||

|
||||
|
||||
When you select it, it will start as an empty, transparent video stream.
|
||||
Then, you can use the following binds to change what it shows:
|
||||
Choose it, then use the following binds to change what it shows.
|
||||
The stream won't start until you make your first target selection.
|
||||
|
||||
- `set-dynamic-cast-window` to cast the focused window.
|
||||
- `set-dynamic-cast-monitor` to cast the focused monitor.
|
||||
- `clear-dynamic-cast-target` to go back to an empty stream.
|
||||
- `clear-dynamic-cast-target` to reset to an empty video stream.
|
||||
|
||||
You can also use these actions from the command line, for example to interactively pick which window to cast:
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
Niri assumes that programs running unsandboxed on the host are **trusted**.
|
||||
|
||||
This is a reasonable assumption because programs running on the host have a wide variety of ways to get all access they need, even without niri.
|
||||
For instance:
|
||||
|
||||
- They can set `$LD_PRELOAD` in `.bashrc` or similar files to load an arbitrary library into all processes.
|
||||
- They can replace binaries in `$PATH` with malicious code.
|
||||
- They can interpose any socket in `$XDG_RUNTIME_DIR`, like Wayland, and do keylogging or record window contents.
|
||||
- They can scan the filesystem for secrets: SSH keys, password stores, etc.
|
||||
- They can connect to an unlocked keyring and steal credentials.
|
||||
- And so on and so forth.
|
||||
|
||||
## Unsandboxed clients
|
||||
|
||||
Anything with access to niri's Wayland socket can, among other things:
|
||||
|
||||
- Record the user's screen via [wlr-screencopy](https://wayland.app/protocols/wlr-screencopy-unstable-v1).
|
||||
- Emulate input via [wlr-virtual-pointer](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard](https://wayland.app/protocols/virtual-keyboard-unstable-v1).
|
||||
- Get the user's clipboard contents via [wlr-data-control](https://wayland.app/protocols/ext-data-control-v1).
|
||||
- Create arbitrary fullscreen surfaces through [wlr-layer-shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) that can steal the user's input, pretend to be a password entry, or lock the user out of their session.
|
||||
- Kill a running lockscreen, create a new lock surface, and tell niri to unlock a locked session.
|
||||
|
||||
Anything with access to niri's [IPC](./IPC.md) socket can, among other things:
|
||||
|
||||
- Spawn a Wayland client which can do everything in the list above.
|
||||
|
||||
Anything with access to niri's D-Bus interfaces can, among other things:
|
||||
|
||||
- Record the user's screen via the screencast interface.
|
||||
- Fully listen to and emulate input from the user's keyboard via the accessibility interface.
|
||||
|
||||
Also, while niri doesn't directly integrate Xwayland, it's worth reminding that anything with access to the X11 `$DISPLAY` (which comes both as a socket file on disk **and** as an abstract socket in the network namespace) can intercept and emulate all input and record the contents of any X11 windows on the same `$DISPLAY` (but not Wayland windows).
|
||||
|
||||
## Running untrusted clients
|
||||
|
||||
Considering all of the above, for running untrusted clients, you need a proper sandbox that:
|
||||
|
||||
- Removes niri's IPC socket.
|
||||
- Prevents D-Bus access to host services.
|
||||
- Uses a filtered Wayland socket.
|
||||
|
||||
For creating a filtered Wayland socket, you can use the [security-context](https://wayland.app/protocols/security-context-v1) protocol which niri implements.
|
||||
All unsafe protocols are made inaccessible through this filtered Wayland socket.
|
||||
|
||||
One sandbox that satisfies all of these criteria is the [Flatpak](https://flatpak.org/) sandbox.
|
||||
|
||||
Importantly, filtering just the Wayland socket (and leaving, for example, unrestricted D-Bus access) is **not enough** to prevent untrusted clients from doing bad things.
|
||||
|
||||
## Lock screen
|
||||
|
||||
When the session is locked via [ext-session-lock](https://wayland.app/protocols/ext-session-lock-v1), most actions (keybindings) are automatically disabled.
|
||||
Only a very small set of safe actions is allowed.
|
||||
In particular, spawning will not work, with the exception of binds explicitly configured with `allow-when-locked=true`.
|
||||
|
||||
Importantly, the **quit** action is allowed—you can always quit niri, even when on a lock screen.
|
||||
Therefore, you must ensure that quitting niri does not drop you into an unprotected TTY commandline.
|
||||
Usually, a display manager, like GDM, will do this for you: when niri exits (via the quit bind or if it crashes), it'll put you back into a safe password prompt.
|
||||
|
||||
Other than quitting, the only way to exit a lock screen is for the lock screen client to tell niri to unlock the session.
|
||||
If the lock screen client crashes, the session remains locked with a solid red background.
|
||||
In this case, another lock screen client can take over (so you can start a fresh lock screen if it crashes, and still unlock your session).
|
||||
@@ -1,11 +1,13 @@
|
||||
### Overview
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
You can apply background effects to windows and layer-shell surfaces.
|
||||
These include blur, xray, saturation, and noise.
|
||||
They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules.
|
||||
|
||||

|
||||
|
||||
The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window).
|
||||
Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this.
|
||||
|
||||
@@ -17,9 +19,9 @@ In this case, the application will usually offer some "background blur" setting
|
||||
You can also enable blur on the niri side with the `blur true` background effect window rule:
|
||||
|
||||
```kdl
|
||||
// Enable blur behind the foot terminal.
|
||||
// Enable blur behind the Alacritty terminal.
|
||||
window-rule {
|
||||
match app-id="^foot$"
|
||||
match app-id="^Alacritty$"
|
||||
|
||||
background-effect {
|
||||
blur true
|
||||
@@ -62,10 +64,11 @@ You can disable xray with `xray false` background effect window rule.
|
||||
This gives you the normal kind of blur where everything below a window is blurred.
|
||||
Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
|
||||
|
||||
Non-xray effects are currently experimental because they have some known limitations.
|
||||
|
||||
- They disappear during window open/close animations and while dragging a tiled window.
|
||||
Fixing this requries a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
|
||||
> [!WARNING]
|
||||
> Non-xray effects are currently experimental because they have some known limitations.
|
||||
>
|
||||
> - They disappear during window open/close animations and while dragging a tiled window.
|
||||
> Fixing this requires a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
|
||||
|
||||
### Implementation notes
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* [Window Effects](./Window-Effects.md)
|
||||
* [Packaging niri](./Packaging-niri.md)
|
||||
* [Integrating niri](./Integrating-niri.md)
|
||||
* [Security Model](./Security-Model.md)
|
||||
* [Accessibility](./Accessibility.md)
|
||||
* [Name and Logo](./Name-and-Logo.md)
|
||||
* [FAQ](./FAQ.md)
|
||||
@@ -42,6 +43,7 @@
|
||||
* [Design Principles](./Development:-Design-Principles.md)
|
||||
* [Developing niri](./Development:-Developing-niri.md)
|
||||
* [Documenting niri](./Development:-Documenting-niri.md)
|
||||
* [Releasing niri](./Development:-Releasing-niri.md)
|
||||
* [Fractional Layout](./Development:-Fractional-Layout.md)
|
||||
* [Redraw Loop](./Development:-Redraw-Loop.md)
|
||||
* [Animation Timing](./Development:-Animation-Timing.md)
|
||||
|
||||
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 9.3 KiB |
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757967192,
|
||||
"narHash": "sha256-/aA9A/OBmnuOMgwfzdsXRusqzUpd8rQnQY8jtrHK+To=",
|
||||
"lastModified": 1781607440,
|
||||
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0d7c15863b251a7a50265e57c1dca1a7add2e291",
|
||||
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,28 +18,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757989933,
|
||||
"narHash": "sha256-9cpKYWWPCFhgwQTww8S94rTXgg8Q8ydFv9fXM6I8xQM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "8249aa3442fb9b45e615a35f39eca2fe5510d7c3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,22 +2,12 @@
|
||||
{
|
||||
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
revision = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
@@ -135,12 +125,12 @@
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/lib/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
# Force linking with libEGL and libwayland-client so they end up in RPATH and
|
||||
# can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
@@ -182,33 +172,20 @@
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
rustfmt' = pkgs.rustfmt.override { asNightly = true; };
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
pkgs.cargo-insta
|
||||
];
|
||||
packages = builtins.attrValues {
|
||||
inherit (pkgs)
|
||||
rustc
|
||||
cargo
|
||||
clippy
|
||||
cargo-insta
|
||||
;
|
||||
inherit rustfmt';
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
@@ -225,8 +202,8 @@
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
# This should only be set with `RUSTFLAGS="$RUSTFLAGS -C your-flags"`
|
||||
RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ bitflags.workspace = true
|
||||
csscolorparser = "0.8.3"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
|
||||
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
|
||||
regex = "1.12.3"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -51,6 +52,9 @@ pub enum Trigger {
|
||||
TouchpadScrollUp,
|
||||
TouchpadScrollLeft,
|
||||
TouchpadScrollRight,
|
||||
TabletStylusButton1,
|
||||
TabletStylusButton2,
|
||||
TabletStylusButton3,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
@@ -769,7 +773,7 @@ where
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
expect_only_children(node, ctx);
|
||||
|
||||
let mut seen_keys = HashSet::new();
|
||||
let mut seen_keys: HashMap<Key, &knuffel::ast::SpannedNode<S>> = HashMap::new();
|
||||
|
||||
let mut binds = Vec::new();
|
||||
|
||||
@@ -779,39 +783,26 @@ where
|
||||
ctx.emit_error(e);
|
||||
}
|
||||
Ok(bind) => {
|
||||
if seen_keys.insert(bind.key) {
|
||||
binds.push(bind);
|
||||
} else {
|
||||
// ideally, this error should point to the previous instance of this keybind
|
||||
//
|
||||
// i (sodiboo) have tried to implement this in various ways:
|
||||
// miette!(), #[derive(Diagnostic)]
|
||||
// DecodeError::Custom, DecodeError::Conversion
|
||||
// nothing seems to work, and i suspect it's not possible.
|
||||
//
|
||||
// DecodeError is fairly restrictive.
|
||||
// even DecodeError::Custom just wraps a std::error::Error
|
||||
// and this erases all rich information from miette. (why???)
|
||||
//
|
||||
// why does knuffel do this?
|
||||
// from what i can tell, it doesn't even use DecodeError for much.
|
||||
// it only ever converts them to a Report anyways!
|
||||
// https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
|
||||
//
|
||||
// besides like, allowing downstream users (such as us!)
|
||||
// to match on parse failure, i don't understand why
|
||||
// it doesn't just use a generic error type
|
||||
//
|
||||
// even the matching isn't consistent,
|
||||
// because errors can also be omitted as ctx.emit_error.
|
||||
// why does *that one* especially, require a DecodeError?
|
||||
//
|
||||
// anyways if you can make it format nicely, definitely do fix this
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind",
|
||||
));
|
||||
match seen_keys.entry(bind.key) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Even though it's technically incorrect, we use
|
||||
// `DecodeError::Missing` here because it labels the bind with
|
||||
// "node starts here", which is the least bad option
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
entry.get(),
|
||||
"keybind first defined here",
|
||||
));
|
||||
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind later defined here",
|
||||
));
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(child);
|
||||
binds.push(bind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,6 +1003,12 @@ impl FromStr for Key {
|
||||
Trigger::TouchpadScrollLeft
|
||||
} else if key.eq_ignore_ascii_case("TouchpadScrollRight") {
|
||||
Trigger::TouchpadScrollRight
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton1") {
|
||||
Trigger::TabletStylusButton1
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton2") {
|
||||
Trigger::TabletStylusButton2
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton3") {
|
||||
Trigger::TabletStylusButton3
|
||||
} else {
|
||||
let mut keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
|
||||
// The keyboard event handling code can receive either
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct Debug {
|
||||
pub enable_overlay_planes: bool,
|
||||
pub disable_cursor_plane: bool,
|
||||
pub disable_direct_scanout: bool,
|
||||
pub keep_max_bpc_unchanged: bool,
|
||||
pub restrict_primary_scanout_to_matching_format: bool,
|
||||
pub force_disable_connectors_on_resume: bool,
|
||||
pub render_drm_device: Option<PathBuf>,
|
||||
@@ -42,8 +41,6 @@ pub struct DebugPart {
|
||||
#[knuffel(child)]
|
||||
pub disable_direct_scanout: Option<Flag>,
|
||||
#[knuffel(child)]
|
||||
pub keep_max_bpc_unchanged: Option<Flag>,
|
||||
#[knuffel(child)]
|
||||
pub restrict_primary_scanout_to_matching_format: Option<Flag>,
|
||||
#[knuffel(child)]
|
||||
pub force_disable_connectors_on_resume: Option<Flag>,
|
||||
@@ -82,7 +79,6 @@ impl MergeWith<DebugPart> for Debug {
|
||||
enable_overlay_planes,
|
||||
disable_cursor_plane,
|
||||
disable_direct_scanout,
|
||||
keep_max_bpc_unchanged,
|
||||
restrict_primary_scanout_to_matching_format,
|
||||
force_disable_connectors_on_resume,
|
||||
force_pipewire_invalid_modifier,
|
||||
|
||||
@@ -364,6 +364,10 @@ pub struct Tablet {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub map_to_output: Option<String>,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_output: bool,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_window: bool,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -340,12 +340,26 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
let path = base.0.join(path);
|
||||
|
||||
// We use DecodeError::Missing throughout this block because it results in the
|
||||
// least confusing error messages while still allowing to provide a span.
|
||||
|
||||
// Expand ~ into the home dir
|
||||
let path = if let Ok(rest) = path.strip_prefix("~") {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("error retrieving home directory to expand {path:?}"),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
home.join(rest)
|
||||
} else {
|
||||
// Otherwise, use the current include base dir
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
base.0.join(path)
|
||||
};
|
||||
|
||||
let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
|
||||
if recursion == RECURSION_LIMIT {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
@@ -705,6 +719,8 @@ mod tests {
|
||||
|
||||
tablet {
|
||||
map-to-output "eDP-1"
|
||||
map-to-focused-output
|
||||
map-to-focused-window
|
||||
calibration-matrix 1.0 2.0 3.0 \
|
||||
4.0 5.0 6.0
|
||||
}
|
||||
@@ -729,6 +745,7 @@ mod tests {
|
||||
transform "flipped-90"
|
||||
position x=10 y=20
|
||||
mode "1920x1080@144"
|
||||
max-bpc 10
|
||||
variable-refresh-rate on-demand=true
|
||||
background-color "rgba(25, 25, 102, 1.0)"
|
||||
hot-corners {
|
||||
@@ -841,7 +858,7 @@ mod tests {
|
||||
window-open { off; }
|
||||
|
||||
window-close {
|
||||
curve "cubic-bezier" 0.05 0.7 0.1 1
|
||||
curve "cubic-bezier" 0.05 0.7 0.1 1
|
||||
}
|
||||
|
||||
recent-windows-close {
|
||||
@@ -1097,6 +1114,8 @@ mod tests {
|
||||
map_to_output: Some(
|
||||
"eDP-1",
|
||||
),
|
||||
map_to_focused_output: true,
|
||||
map_to_focused_window: true,
|
||||
left_handed: false,
|
||||
},
|
||||
touch: Touch {
|
||||
@@ -1142,6 +1161,11 @@ mod tests {
|
||||
y: 20,
|
||||
},
|
||||
),
|
||||
max_bpc: Some(
|
||||
MaxBpc(
|
||||
_10,
|
||||
),
|
||||
),
|
||||
mode: Some(
|
||||
Mode {
|
||||
custom: false,
|
||||
@@ -1187,6 +1211,7 @@ mod tests {
|
||||
scale: None,
|
||||
transform: Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: Some(
|
||||
Mode {
|
||||
custom: true,
|
||||
@@ -1213,6 +1238,7 @@ mod tests {
|
||||
scale: None,
|
||||
transform: Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: None,
|
||||
modeline: Some(
|
||||
Modeline {
|
||||
@@ -2226,7 +2252,6 @@ mod tests {
|
||||
enable_overlay_planes: false,
|
||||
disable_cursor_plane: false,
|
||||
disable_direct_scanout: false,
|
||||
keep_max_bpc_unchanged: false,
|
||||
restrict_primary_scanout_to_matching_format: false,
|
||||
force_disable_connectors_on_resume: false,
|
||||
render_drm_device: Some(
|
||||
|
||||
@@ -59,6 +59,8 @@ pub struct Output {
|
||||
pub transform: Transform,
|
||||
#[knuffel(child)]
|
||||
pub position: Option<Position>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub max_bpc: Option<MaxBpc>,
|
||||
#[knuffel(child)]
|
||||
pub mode: Option<Mode>,
|
||||
#[knuffel(child)]
|
||||
@@ -101,6 +103,7 @@ impl Default for Output {
|
||||
scale: None,
|
||||
transform: Transform::Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: None,
|
||||
modeline: None,
|
||||
variable_refresh_rate: None,
|
||||
@@ -128,6 +131,9 @@ pub struct Position {
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct MaxBpc(pub niri_ipc::MaxBpc);
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)]
|
||||
pub struct Vrr {
|
||||
#[knuffel(property, default = false)]
|
||||
@@ -257,6 +263,42 @@ impl OutputName {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ErrorSpan> knuffel::DecodeScalar<S> for MaxBpc {
|
||||
fn type_check(
|
||||
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
|
||||
ctx: &mut Context<S>,
|
||||
) {
|
||||
if let Some(type_name) = &type_name {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
type_name,
|
||||
"type name",
|
||||
"no type name expected for this node",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_decode(
|
||||
value: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
|
||||
ctx: &mut Context<S>,
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
match &**value {
|
||||
knuffel::ast::Literal::Int(ref val) => match u8::try_from(val) {
|
||||
Ok(v) => niri_ipc::MaxBpc::try_from(v)
|
||||
.map(MaxBpc)
|
||||
.map_err(|e| DecodeError::conversion(value, e)),
|
||||
Err(e) => {
|
||||
ctx.emit_error(DecodeError::conversion(value, e));
|
||||
Ok(Self::default())
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
ctx.emit_error(DecodeError::scalar_kind(knuffel::decode::Kind::Int, value));
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ErrorSpan> knuffel::Decode<S> for Mode {
|
||||
fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> {
|
||||
if let Some(type_name) = &node.type_name {
|
||||
|
||||
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.11.0"
|
||||
niri-ipc = "=26.4.0"
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.11.0"
|
||||
//! niri-ipc = "=26.4.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -1097,6 +1097,12 @@ pub enum OutputAction {
|
||||
#[cfg_attr(feature = "clap", command(flatten))]
|
||||
vrr: VrrToSet,
|
||||
},
|
||||
/// Set the maximum bits per channel (bit depth).
|
||||
MaxBpc {
|
||||
/// Maximum bits per channel to set.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
max_bpc: MaxBpc,
|
||||
},
|
||||
}
|
||||
|
||||
/// Output mode to set.
|
||||
@@ -1228,6 +1234,8 @@ pub struct Output {
|
||||
///
|
||||
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
|
||||
pub logical: Option<LogicalOutput>,
|
||||
/// Maximum bits per channel (bit depth), if known.
|
||||
pub max_bpc: Option<u8>,
|
||||
}
|
||||
|
||||
/// Output mode.
|
||||
@@ -1291,6 +1299,32 @@ pub enum Transform {
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
/// Output maximum bits per channel.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum MaxBpc {
|
||||
/// 6-bit.
|
||||
#[serde(rename = "6")]
|
||||
_6 = 6,
|
||||
/// 8-bit.
|
||||
#[default]
|
||||
#[serde(rename = "8")]
|
||||
_8 = 8,
|
||||
/// 10-bit.
|
||||
#[serde(rename = "10")]
|
||||
_10 = 10,
|
||||
/// 12-bit.
|
||||
#[serde(rename = "12")]
|
||||
_12 = 12,
|
||||
/// 14-bit.
|
||||
#[serde(rename = "14")]
|
||||
_14 = 14,
|
||||
/// 16-bit.
|
||||
#[serde(rename = "16")]
|
||||
_16 = 16,
|
||||
}
|
||||
|
||||
/// Toplevel window.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1868,6 +1902,30 @@ impl FromStr for Transform {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MaxBpc {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
6 => Ok(MaxBpc::_6),
|
||||
8 => Ok(MaxBpc::_8),
|
||||
10 => Ok(MaxBpc::_10),
|
||||
12 => Ok(MaxBpc::_12),
|
||||
14 => Ok(MaxBpc::_14),
|
||||
16 => Ok(MaxBpc::_16),
|
||||
_ => Err("invalid max-bpc, can be 6, 8, 10, 12, 14, 16"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MaxBpc {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s.parse::<u8>().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Layer {
|
||||
type Err = &'static str;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ repository.workspace = true
|
||||
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.11.0", path = ".." }
|
||||
niri-config = { version = "25.11.0", path = "../niri-config" }
|
||||
niri = { version = "26.4.0", path = ".." }
|
||||
niri-config = { version = "26.4.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -38,7 +38,6 @@ SourceLicense: GPL-3.0-or-later
|
||||
# 0BSD OR MIT OR Apache-2.0
|
||||
# Apache-2.0
|
||||
# Apache-2.0 AND MIT
|
||||
# Apache-2.0 OR BSL-1.0
|
||||
# Apache-2.0 OR MIT
|
||||
# Apache-2.0 OR MIT OR Unlicense
|
||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||
@@ -53,11 +52,10 @@ SourceLicense: GPL-3.0-or-later
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unicode-3.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) 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 OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) 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) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND Apache-2.0 AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) 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 OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) 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 AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/niri-wm/niri
|
||||
|
||||
@@ -134,7 +134,7 @@ layout {
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
@@ -383,6 +383,7 @@ binds {
|
||||
// Example media keys mapping using playerctl.
|
||||
// This will work with any MPRIS-enabled media player.
|
||||
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; }
|
||||
XF86AudioPause allow-when-locked=true { spawn-sh "playerctl play-pause"; }
|
||||
XF86AudioStop allow-when-locked=true { spawn-sh "playerctl stop"; }
|
||||
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; }
|
||||
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; }
|
||||
@@ -550,11 +551,14 @@ binds {
|
||||
// Expel the bottom window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// Cycle through widths set in preset-column-widths.
|
||||
Mod+R { switch-preset-column-width; }
|
||||
// Cycling through the presets in reverse order is also possible.
|
||||
// Mod+R { switch-preset-column-width-back; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Shift+R { switch-preset-column-width-back; }
|
||||
|
||||
Mod+Ctrl+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ if [ -n "$SHELL" ] &&
|
||||
! (echo "$SHELL" | grep -q "false") &&
|
||||
! (echo "$SHELL" | grep -q "nologin"); then
|
||||
if [ "$1" != '-l' ]; then
|
||||
exec bash -c "exec -l '$SHELL' -c '$0 -l $*'"
|
||||
exec bash -c "exec -l '$SHELL' -c 'exec $0 -l $*'"
|
||||
else
|
||||
shift
|
||||
fi
|
||||
|
||||
@@ -109,6 +109,7 @@ impl Headless {
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
max_bpc: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::{anyhow, bail, ensure, Context};
|
||||
use bytemuck::cast_slice_mut;
|
||||
use drm_ffi::drm_mode_modeinfo;
|
||||
use libc::dev_t;
|
||||
use niri_config::output::Modeline;
|
||||
use niri_config::output::{MaxBpc, Modeline};
|
||||
use niri_config::{Config, OutputName};
|
||||
use niri_ipc::{HSyncPolarity, VSyncPolarity};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
@@ -70,7 +70,11 @@ use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
|
||||
const SUPPORTED_COLOR_FORMATS: [Fourcc; 8] = [
|
||||
Fourcc::Xrgb2101010,
|
||||
Fourcc::Xbgr2101010,
|
||||
Fourcc::Argb2101010,
|
||||
Fourcc::Abgr2101010,
|
||||
Fourcc::Xrgb8888,
|
||||
Fourcc::Xbgr8888,
|
||||
Fourcc::Argb8888,
|
||||
@@ -97,9 +101,6 @@ pub struct Tty {
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
// The output config had changed, but the session is paused, so we need to update it on resume.
|
||||
update_output_config_on_resume: bool,
|
||||
// The ignored nodes have changed, but the session is paused, so we need to update it on
|
||||
// resume.
|
||||
update_ignored_nodes_on_resume: bool,
|
||||
// Whether the debug tinting is enabled.
|
||||
debug_tint: bool,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -408,6 +409,8 @@ struct ConnectorProperties<'a> {
|
||||
device: &'a DrmDevice,
|
||||
connector: connector::Handle,
|
||||
properties: Vec<(property::Info, property::RawValue)>,
|
||||
has_change: bool,
|
||||
requests: AtomicModeReq,
|
||||
}
|
||||
|
||||
impl Tty {
|
||||
@@ -441,6 +444,14 @@ impl Tty {
|
||||
}
|
||||
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
||||
|
||||
// If the session is not active at startup (e.g. niri was launched from a different TTY),
|
||||
// suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
|
||||
// full re-enumeration of input devices instead of being a no-op.
|
||||
if !session.is_active() {
|
||||
debug!("session is not active, starting libinput in paused state");
|
||||
libinput.suspend();
|
||||
}
|
||||
|
||||
let input_backend = LibinputInputBackend::new(libinput.clone());
|
||||
event_loop
|
||||
.insert_source(input_backend, |mut event, _, state| {
|
||||
@@ -487,11 +498,6 @@ impl Tty {
|
||||
}
|
||||
info!("using as the render node: {node_path}");
|
||||
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
|
||||
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
session,
|
||||
@@ -500,17 +506,27 @@ impl Tty {
|
||||
gpu_manager,
|
||||
primary_node,
|
||||
primary_render_node,
|
||||
ignored_nodes,
|
||||
ignored_nodes: HashSet::new(),
|
||||
devices: HashMap::new(),
|
||||
dmabuf_global: None,
|
||||
update_output_config_on_resume: false,
|
||||
update_ignored_nodes_on_resume: false,
|
||||
debug_tint: false,
|
||||
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
// If the session is inactive, skip initialization because we won't be able to do much with
|
||||
// the devices anyway. We'll get ActivateSession and add the devices there instead.
|
||||
//
|
||||
// This can happen when starting niri while having a different TTY active (e.g. via tmux).
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the ignored nodes.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let udev = self.udev_dispatcher.clone();
|
||||
let udev = udev.as_source_ref();
|
||||
|
||||
@@ -550,6 +566,10 @@ impl Tty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompute ignored nodes to resolve symlinks (like /dev/dri/by-path/...) to their
|
||||
// new underlying device IDs.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -597,16 +617,9 @@ impl Tty {
|
||||
warn!("error resuming libinput");
|
||||
}
|
||||
|
||||
if self.update_ignored_nodes_on_resume {
|
||||
self.update_ignored_nodes_on_resume = false;
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
self.ignored_nodes = ignored_nodes;
|
||||
}
|
||||
// While the session was suspended, GPUs could have been added, so
|
||||
// /dev/dri/by-path/... symlinks need to be re-resolved.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let mut device_list = self
|
||||
.udev_dispatcher
|
||||
@@ -669,16 +682,19 @@ impl Tty {
|
||||
// Apply pending gamma changes and restore our existing gamma.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
if let Ok(props) =
|
||||
if let Ok(mut props) =
|
||||
ConnectorProperties::try_new(&device.drm, surface.connector)
|
||||
{
|
||||
match reset_hdr(&props) {
|
||||
Ok(()) => (),
|
||||
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
||||
}
|
||||
let max_bpc = self
|
||||
.config
|
||||
.borrow()
|
||||
.outputs
|
||||
.find(&surface.name)
|
||||
.and_then(|o| o.max_bpc);
|
||||
set_connector_properties(&mut props, max_bpc, true);
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(ramp) = surface.pending_gamma_change.take() {
|
||||
let ramp = ramp.as_deref();
|
||||
@@ -699,7 +715,14 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Add new devices.
|
||||
for (device_id, path) in device_list.into_iter() {
|
||||
//
|
||||
// Add the primary node first as later nodes might depend on the primary render
|
||||
// node being available.
|
||||
let primary_device_id = self.primary_node.dev_id();
|
||||
let primary_device_path = device_list.remove(&primary_device_id);
|
||||
let primary = primary_device_path.map(|path| (primary_device_id, path));
|
||||
|
||||
for (device_id, path) in primary.into_iter().chain(device_list) {
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -809,7 +832,10 @@ impl Tty {
|
||||
.context("error creating renderer")?;
|
||||
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding wl-display in EGL: {err:?}");
|
||||
// wl_drm is on its way out so this is expected on most modern distros.
|
||||
trace!("error binding legacy EGL to wl_display: {err}");
|
||||
} else {
|
||||
debug!("bound legacy EGL to wl_display");
|
||||
}
|
||||
|
||||
let gles_renderer = renderer.as_gles_renderer();
|
||||
@@ -1285,13 +1311,10 @@ impl Tty {
|
||||
debug!("picking mode: {mode:?}");
|
||||
|
||||
let mut orientation = None;
|
||||
if let Ok(props) = ConnectorProperties::try_new(&device.drm, connector.handle()) {
|
||||
match reset_hdr(&props) {
|
||||
Ok(()) => (),
|
||||
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
||||
}
|
||||
if let Ok(mut props) = ConnectorProperties::try_new(&device.drm, connector.handle()) {
|
||||
set_connector_properties(&mut props, config.max_bpc, true);
|
||||
|
||||
match get_panel_orientation(&props) {
|
||||
match props.get_panel_orientation() {
|
||||
Ok(x) => orientation = Some(x),
|
||||
Err(err) => {
|
||||
trace!("couldn't get panel orientation: {err:?}");
|
||||
@@ -1299,7 +1322,7 @@ impl Tty {
|
||||
}
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
};
|
||||
}
|
||||
|
||||
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
||||
.map_err(|err| debug!("couldn't get gamma properties: {err:?}"))
|
||||
@@ -1419,7 +1442,7 @@ impl Tty {
|
||||
|
||||
// Create the compositor.
|
||||
let res = DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -1449,7 +1472,7 @@ impl Tty {
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
|
||||
DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -2177,6 +2200,15 @@ impl Tty {
|
||||
OutputId::next()
|
||||
});
|
||||
|
||||
let props = ConnectorProperties::try_new(&device.drm, connector.handle()).ok();
|
||||
let max_bpc = props.as_ref().and_then(|p| p.find(c"max bpc").ok());
|
||||
let max_bpc = max_bpc.and_then(|(info, value)| {
|
||||
info.value_type()
|
||||
.convert_value(*value)
|
||||
.as_unsigned_range()
|
||||
.map(|v| v as u8)
|
||||
});
|
||||
|
||||
let ipc_output = niri_ipc::Output {
|
||||
name: connector_name,
|
||||
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
||||
@@ -2189,6 +2221,7 @@ impl Tty {
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
max_bpc,
|
||||
};
|
||||
|
||||
ipc_outputs.insert(id, ipc_output);
|
||||
@@ -2266,22 +2299,25 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, so just set a flag for later.
|
||||
if !self.session.is_active() {
|
||||
self.update_ignored_nodes_on_resume = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fn compute_ignored_nodes(&self) -> HashSet<DrmNode> {
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
ignored_nodes
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, but we'll recompute in ActivateSession.
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ignored_nodes = self.compute_ignored_nodes();
|
||||
if ignored_nodes == self.ignored_nodes {
|
||||
return;
|
||||
}
|
||||
@@ -2402,6 +2438,13 @@ impl Tty {
|
||||
},
|
||||
};
|
||||
|
||||
if let Ok(mut props) = ConnectorProperties::try_new(&device.drm, surface.connector)
|
||||
{
|
||||
set_connector_properties(&mut props, config.max_bpc, false);
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
}
|
||||
|
||||
let change_mode = surface.compositor.pending_mode() != mode;
|
||||
|
||||
let vrr_enabled = surface.compositor.vrr_enabled();
|
||||
@@ -3230,6 +3273,8 @@ impl<'a> ConnectorProperties<'a> {
|
||||
device,
|
||||
connector,
|
||||
properties,
|
||||
has_change: false,
|
||||
requests: AtomicModeReq::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3242,35 +3287,115 @@ impl<'a> ConnectorProperties<'a> {
|
||||
|
||||
Err(anyhow!("couldn't find property: {name:?}"))
|
||||
}
|
||||
|
||||
fn get_panel_orientation(&self) -> anyhow::Result<Transform> {
|
||||
let (info, value) = self.find(c"panel orientation")?;
|
||||
match info.value_type().convert_value(*value) {
|
||||
property::Value::Enum(Some(val)) => match val.value() {
|
||||
// "Normal"
|
||||
0 => Ok(Transform::Normal),
|
||||
// "Upside Down"
|
||||
1 => Ok(Transform::_180),
|
||||
// "Left Side Up"
|
||||
2 => Ok(Transform::_90),
|
||||
// "Right Side Up"
|
||||
3 => Ok(Transform::_270),
|
||||
_ => bail!("panel orientation has invalid value: {:?}", val),
|
||||
},
|
||||
_ => bail!("panel orientation has wrong value type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_hdr(&mut self) -> anyhow::Result<()> {
|
||||
const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
|
||||
|
||||
let (info, value) = self.find(c"HDR_OUTPUT_METADATA")?;
|
||||
|
||||
let property::ValueType::Blob = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != 0 {
|
||||
self.requests
|
||||
.add_raw_property(self.connector.into(), info.handle(), 0);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
let (info, value) = self.find(c"Colorspace")?;
|
||||
let property::ValueType::Enum(_) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != DRM_MODE_COLORIMETRY_DEFAULT {
|
||||
self.requests.add_raw_property(
|
||||
self.connector.into(),
|
||||
info.handle(),
|
||||
DRM_MODE_COLORIMETRY_DEFAULT,
|
||||
);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_max_bpc(&mut self, max_bpc: MaxBpc) -> anyhow::Result<u64> {
|
||||
let (info, value) = self.find(c"max bpc")?;
|
||||
|
||||
let property::ValueType::UnsignedRange(min, max) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
let max_bpc = max_bpc.0 as u64;
|
||||
if !(min..=max).contains(&max_bpc) {
|
||||
bail!("max-bpc {max_bpc} outside valid range of [{min}, {max}]");
|
||||
}
|
||||
|
||||
let property::Value::UnsignedRange(value) = info.value_type().convert_value(*value) else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
if value != max_bpc {
|
||||
self.requests.add_raw_property(
|
||||
self.connector.into(),
|
||||
info.handle(),
|
||||
property::Value::UnsignedRange(max_bpc).into(),
|
||||
);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
Ok(max_bpc)
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> anyhow::Result<()> {
|
||||
if self.has_change {
|
||||
self.device.atomic_commit(
|
||||
AtomicCommitFlags::ALLOW_MODESET,
|
||||
std::mem::take(&mut self.requests),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
|
||||
|
||||
fn reset_hdr(props: &ConnectorProperties) -> anyhow::Result<()> {
|
||||
let (info, value) = props.find(c"HDR_OUTPUT_METADATA")?;
|
||||
let property::ValueType::Blob = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
if *value != 0 {
|
||||
props
|
||||
.device
|
||||
.set_property(props.connector, info.handle(), 0)
|
||||
.context("error setting property")?;
|
||||
fn set_connector_properties(
|
||||
props: &mut ConnectorProperties,
|
||||
max_bpc: Option<MaxBpc>,
|
||||
reset_hdr: bool,
|
||||
) {
|
||||
if let Some(max_bpc) = max_bpc {
|
||||
if let Err(err) = props.set_max_bpc(max_bpc) {
|
||||
debug!("failed to set `max bpc` property: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let (info, value) = props.find(c"Colorspace")?;
|
||||
let property::ValueType::Enum(_) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != DRM_MODE_COLORIMETRY_DEFAULT {
|
||||
props
|
||||
.device
|
||||
.set_property(props.connector, info.handle(), DRM_MODE_COLORIMETRY_DEFAULT)
|
||||
.context("error setting property")?;
|
||||
if reset_hdr {
|
||||
if let Err(err) = props.reset_hdr() {
|
||||
debug!("failed to set HDR properties: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if let Err(err) = props.commit() {
|
||||
warn!("failed to atomically commit properties: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bool> {
|
||||
@@ -3278,24 +3403,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
|
||||
info.value_type().convert_value(value).as_boolean()
|
||||
}
|
||||
|
||||
fn get_panel_orientation(props: &ConnectorProperties) -> anyhow::Result<Transform> {
|
||||
let (info, value) = props.find(c"panel orientation")?;
|
||||
match info.value_type().convert_value(*value) {
|
||||
property::Value::Enum(Some(val)) => match val.value() {
|
||||
// "Normal"
|
||||
0 => Ok(Transform::Normal),
|
||||
// "Upside Down"
|
||||
1 => Ok(Transform::_180),
|
||||
// "Left Side Up"
|
||||
2 => Ok(Transform::_90),
|
||||
// "Right Side Up"
|
||||
3 => Ok(Transform::_270),
|
||||
_ => bail!("panel orientation has invalid value: {:?}", val),
|
||||
},
|
||||
_ => bail!("panel orientation has wrong value type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_gamma_for_crtc(
|
||||
device: &DrmDevice,
|
||||
crtc: crtc::Handle,
|
||||
|
||||
@@ -4,8 +4,10 @@ use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::egl::EGLDevice;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
@@ -16,6 +18,7 @@ use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_pre
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::platform::wayland::WindowAttributesExtWayland;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal};
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
@@ -29,6 +32,7 @@ pub struct Winit {
|
||||
output: Output,
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
@@ -91,6 +95,7 @@ impl Winit {
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
max_bpc: None,
|
||||
},
|
||||
)])));
|
||||
|
||||
@@ -137,6 +142,7 @@ impl Winit {
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
dmabuf_global: None,
|
||||
ipc_outputs,
|
||||
})
|
||||
}
|
||||
@@ -144,7 +150,10 @@ impl Winit {
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
// wl_drm is on its way out so this is expected on most modern distros.
|
||||
trace!("error binding legacy EGL to wl_display: {err}");
|
||||
} else {
|
||||
debug!("bound legacy EGL to wl_display");
|
||||
}
|
||||
|
||||
resources::init(renderer);
|
||||
@@ -164,9 +173,44 @@ impl Winit {
|
||||
|
||||
niri.update_shaders();
|
||||
|
||||
self.create_dmabuf_global(niri);
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
|
||||
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
|
||||
let default_feedback = || {
|
||||
let display = renderer.egl_context().display();
|
||||
let device =
|
||||
EGLDevice::device_for_display(display).context("error getting EGL device")?;
|
||||
let node = device
|
||||
.try_get_render_node()
|
||||
.context("error getting EGL device render node")?
|
||||
.context("failed to query EGL device render node")?;
|
||||
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
|
||||
.build()
|
||||
.context("error building dmabuf feedback")
|
||||
};
|
||||
|
||||
// Fallback to dmabuf v3 if we failed to build feedback.
|
||||
let dmabuf_global = match default_feedback() {
|
||||
Ok(feedback) => niri
|
||||
.dmabuf_state
|
||||
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
|
||||
Err(err) => {
|
||||
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
niri.dmabuf_state
|
||||
.create_global::<State>(&niri.display_handle, primary_formats)
|
||||
}
|
||||
};
|
||||
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"winit".to_owned()
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ impl CompositorHandler for State {
|
||||
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
|
||||
// subsequent toplevel_destroyed() call.
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,13 +572,13 @@ impl State {
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
remove_pre_commit_hook(surface, &prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
|
||||
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
|
||||
namespace: String,
|
||||
) {
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
self.niri.output_from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
|
||||
@@ -361,7 +361,7 @@ impl DndGrabHandler for State {
|
||||
trace!("dnd dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
|
||||
// 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.
|
||||
@@ -383,10 +383,21 @@ impl DndGrabHandler for State {
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
|
||||
trace!("dnd cancelled");
|
||||
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::niri::Niri {
|
||||
fn on_maybe_dnd_ended(&mut self) {
|
||||
self.layout.dnd_end();
|
||||
self.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +472,7 @@ impl SessionLockHandler for State {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
let Some(output) = self.niri.output_from_resource(&output) else {
|
||||
warn!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
@@ -546,7 +557,9 @@ impl ForeignToplevelHandler for State {
|
||||
{
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if let Some(requested_output) =
|
||||
wl_output.and_then(|o| self.niri.output_from_resource(&o))
|
||||
{
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
@@ -622,6 +635,12 @@ delegate_ext_workspace!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// This can happen if the output was removed before this was called.
|
||||
if !self.niri.output_exists(screencopy.output()) {
|
||||
trace!("screencopy output no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
|
||||
@@ -612,7 +612,7 @@ impl XdgShellHandler for State {
|
||||
toplevel: ToplevelSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
) {
|
||||
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
|
||||
|
||||
if let Some((mapped, current_output)) = self
|
||||
.niri
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::any::Any;
|
||||
use std::cmp::min;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
@@ -41,6 +40,8 @@ use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||
use touch_overview_grab::TouchOverviewGrab;
|
||||
|
||||
use self::move_grab::MoveGrab;
|
||||
use self::pick_color_grab::PickColorGrab;
|
||||
use self::pick_window_grab::PickWindowGrab;
|
||||
use self::resize_grab::ResizeGrab;
|
||||
use self::spatial_movement_grab::SpatialMovementGrab;
|
||||
#[cfg(feature = "dbus")]
|
||||
@@ -293,42 +294,70 @@ impl State {
|
||||
I::Device: 'static,
|
||||
{
|
||||
let device_output = event.device().output(self);
|
||||
let device_output = device_output.filter(|output| self.niri.output_exists(output));
|
||||
let device_output = device_output.as_ref();
|
||||
let (target_geo, keep_ratio, px, transform) =
|
||||
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
|
||||
(
|
||||
self.niri.global_space.output_geometry(output).unwrap(),
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else {
|
||||
let geo = self.global_bounding_rectangle()?;
|
||||
let mapped_output = device_output.or_else(|| self.niri.output_for_tablet());
|
||||
|
||||
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
|
||||
// corresponding to the position on the right when clamping.
|
||||
let output = self.niri.global_space.outputs().next().unwrap();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
// If the tablet is configured to map to the focused window, use that window's geometry on
|
||||
// the mapped output (or on the focused output if no specific output is mapped).
|
||||
let map_to_focused_window = self.niri.config.borrow().input.tablet.map_to_focused_window;
|
||||
// But only if the keyboard focus is on the layout, so that it doesn't trigger on the lock
|
||||
// screen and such.
|
||||
let window_target = if map_to_focused_window && self.niri.keyboard_focus.is_layout() {
|
||||
let output = mapped_output.or_else(|| self.niri.layout.active_output());
|
||||
output.and_then(|output| {
|
||||
let monitor = self.niri.layout.monitor_for_output(output)?;
|
||||
let mut rect = monitor.active_window_visual_rectangle()?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output)?;
|
||||
rect.loc += output_geo.loc.to_f64();
|
||||
Some((rect, output))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
|
||||
(geo, false, 1. / scale, Transform::Normal)
|
||||
};
|
||||
let (target_geo, keep_ratio, px, transform) = if let Some((rect, output)) = window_target {
|
||||
(
|
||||
rect,
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else if let Some(output) = mapped_output {
|
||||
let geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
(
|
||||
geo.to_f64(),
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else {
|
||||
let geo = self.global_bounding_rectangle()?.to_f64();
|
||||
|
||||
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
|
||||
// corresponding to the position on the right when clamping.
|
||||
let output = self.niri.global_space.outputs().next().unwrap();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
|
||||
(geo, false, 1. / scale, Transform::Normal)
|
||||
};
|
||||
|
||||
let mut pos = {
|
||||
let size = transform.invert().transform_size(target_geo.size);
|
||||
transform.transform_point_in(event.position_transformed(size), &size.to_f64())
|
||||
transform.transform_point_in(event.position_transformed(size.to_i32_round()), &size)
|
||||
};
|
||||
|
||||
if keep_ratio {
|
||||
pos.x /= target_geo.size.w as f64;
|
||||
pos.y /= target_geo.size.h as f64;
|
||||
pos.x /= target_geo.size.w;
|
||||
pos.y /= target_geo.size.h;
|
||||
|
||||
let device = event.device();
|
||||
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
|
||||
if let Some(data) = self.niri.tablets.get(device) {
|
||||
// This code does the same thing as mutter with "keep aspect ratio" enabled.
|
||||
let size = transform.invert().transform_size(target_geo.size);
|
||||
let output_aspect_ratio = size.w as f64 / size.h as f64;
|
||||
let output_aspect_ratio = size.w / size.h;
|
||||
let ratio = data.aspect_ratio / output_aspect_ratio;
|
||||
|
||||
if ratio > 1. {
|
||||
@@ -339,13 +368,13 @@ impl State {
|
||||
}
|
||||
};
|
||||
|
||||
pos.x *= target_geo.size.w as f64;
|
||||
pos.y *= target_geo.size.h as f64;
|
||||
pos.x *= target_geo.size.w;
|
||||
pos.y *= target_geo.size.h;
|
||||
}
|
||||
|
||||
pos.x = pos.x.clamp(0.0, target_geo.size.w as f64 - px);
|
||||
pos.y = pos.y.clamp(0.0, target_geo.size.h as f64 - px);
|
||||
Some(pos + target_geo.loc.to_f64())
|
||||
pos.x = pos.x.clamp(0.0, target_geo.size.w - px);
|
||||
pos.y = pos.y.clamp(0.0, target_geo.size.h - px);
|
||||
Some(pos + target_geo.loc)
|
||||
}
|
||||
|
||||
fn is_inhibiting_shortcuts(&self) -> bool {
|
||||
@@ -488,19 +517,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if pressed
|
||||
&& raw == Some(Keysym::Escape)
|
||||
&& (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
|
||||
{
|
||||
// We window picking state so the pick window grab must be active.
|
||||
// Unsetting it cancels window picking.
|
||||
this.niri
|
||||
.seat
|
||||
.get_pointer()
|
||||
.unwrap()
|
||||
.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
if pressed && raw == Some(Keysym::Escape) {
|
||||
// Cancel certain grabs on Escape.
|
||||
let pointer = this.niri.seat.get_pointer().unwrap();
|
||||
if pointer
|
||||
.with_grab(|_, grab| Self::grab_can_be_cancelled_with_esc(grab))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
pointer.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Keysym::space) = raw {
|
||||
@@ -2519,16 +2546,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (new_pos - geom.loc.to_f64())
|
||||
let point = (new_pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -2656,16 +2677,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -2750,10 +2765,11 @@ impl State {
|
||||
return;
|
||||
}
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mod_down = modifiers.contains(mod_key.to_modifiers());
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
let mut is_mru_open = false;
|
||||
if let Some(mru_output) = self.niri.window_mru_ui.output() {
|
||||
is_mru_open = true;
|
||||
@@ -2790,6 +2806,9 @@ impl State {
|
||||
let bindings =
|
||||
make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers);
|
||||
find_configured_bind(bindings, mod_key, trigger, mods)
|
||||
})
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open() || allowed_during_screenshot(&bind.action)
|
||||
}) {
|
||||
self.niri.suppressed_buttons.insert(button_code);
|
||||
self.handle_bind(bind.clone());
|
||||
@@ -2831,44 +2850,41 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() && mod_down {
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_id = ws.id();
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
|
||||
let location = pointer.current_location();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_id = ws.id();
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
|
||||
let location = pointer.current_location();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
// Don't activate the window under the cursor to avoid unnecessary
|
||||
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
|
||||
return;
|
||||
}
|
||||
// Don't activate the window under the cursor to avoid unnecessary
|
||||
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2877,7 +2893,6 @@ impl State {
|
||||
|
||||
// Check if we need to start an interactive move.
|
||||
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if is_overview_open || mod_down {
|
||||
let location = pointer.current_location();
|
||||
|
||||
@@ -2911,72 +2926,69 @@ impl State {
|
||||
}
|
||||
}
|
||||
// Check if we need to start an interactive resize.
|
||||
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let edges = self
|
||||
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() && mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let edges = self
|
||||
.niri
|
||||
.layout
|
||||
.resize_edges_under(output, pos_within_output)
|
||||
.unwrap_or(ResizeEdge::empty());
|
||||
|
||||
if !edges.is_empty() {
|
||||
// See if we got a double resize-click gesture.
|
||||
// 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();
|
||||
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.
|
||||
last_cell.set(None);
|
||||
|
||||
let intersection = edges.intersection(last_edges);
|
||||
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
// FIXME: don't activate once we can pass specific windows
|
||||
// to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.toggle_full_width();
|
||||
}
|
||||
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.reset_window_height(Some(&window));
|
||||
}
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.layout
|
||||
.resize_edges_under(output, pos_within_output)
|
||||
.unwrap_or(ResizeEdge::empty());
|
||||
|
||||
if !edges.is_empty() {
|
||||
// See if we got a double resize-click gesture.
|
||||
// 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();
|
||||
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.
|
||||
last_cell.set(None);
|
||||
|
||||
let intersection = edges.intersection(last_edges);
|
||||
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
// FIXME: don't activate once we can pass specific windows
|
||||
// to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.toggle_full_width();
|
||||
}
|
||||
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.reset_window_height(Some(&window));
|
||||
}
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.cursor_manager.set_cursor_image(
|
||||
CursorImageStatus::Named(edges.cursor_icon()),
|
||||
);
|
||||
}
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(edges.cursor_icon()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3016,20 +3028,25 @@ impl State {
|
||||
if button == Some(MouseButton::Left) && self.niri.screenshot_ui.is_open() {
|
||||
if button_state == ButtonState::Pressed {
|
||||
let pos = pointer.current_location();
|
||||
if let Some((output, _)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
self.niri.output_under(pos).map(|(out, _)| out)
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self.niri.screenshot_ui.pointer_down(output, point, None) {
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, None, mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -3145,13 +3162,21 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::WheelScrollLeft,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_right = find_configured_bind(
|
||||
bindings,
|
||||
mod_key,
|
||||
Trigger::WheelScrollRight,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
(bind_left, bind_right)
|
||||
};
|
||||
|
||||
@@ -3232,9 +3257,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::WheelScrollUp,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_down =
|
||||
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
(bind_up, bind_down)
|
||||
};
|
||||
|
||||
@@ -3260,8 +3293,8 @@ impl State {
|
||||
let horizontal_amount = event.amount(Axis::Horizontal);
|
||||
let vertical_amount = event.amount(Axis::Vertical);
|
||||
|
||||
// Handle touchpad scroll bindings.
|
||||
if source == AxisSource::Finger {
|
||||
// Handle touchpad and continuous scroll bindings.
|
||||
if source == AxisSource::Finger || source == AxisSource::Continuous {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
|
||||
@@ -3377,9 +3410,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::TouchpadScrollLeft,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_right =
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
drop(config);
|
||||
|
||||
if let Some(right) = bind_right {
|
||||
@@ -3407,9 +3448,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::TouchpadScrollUp,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_down =
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
drop(config);
|
||||
|
||||
if let Some(down) = bind_down {
|
||||
@@ -3513,16 +3562,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -3595,19 +3638,29 @@ impl State {
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
if self.niri.screenshot_ui.is_open() {
|
||||
if let Some(output) = under.output.clone() {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mod_down = modifiers.contains(mod_key.to_modifiers());
|
||||
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
under.output.as_ref()
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self.niri.screenshot_ui.pointer_down(output, point, None) {
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, None, mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -3725,11 +3778,53 @@ impl State {
|
||||
}
|
||||
|
||||
fn on_tablet_tool_button<I: InputBackend>(&mut self, event: I::TabletToolButtonEvent) {
|
||||
const BTN_STYLUS3: u32 = 0x149;
|
||||
const BTN_STYLUS: u32 = 0x14b;
|
||||
const BTN_STYLUS2: u32 = 0x14c;
|
||||
|
||||
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
|
||||
|
||||
if let Some(tool) = tool {
|
||||
let button = event.button();
|
||||
|
||||
if self.niri.suppressed_buttons.remove(&button) {
|
||||
return;
|
||||
}
|
||||
|
||||
let trigger = match button {
|
||||
BTN_STYLUS => Some(Trigger::TabletStylusButton1),
|
||||
BTN_STYLUS2 => Some(Trigger::TabletStylusButton2),
|
||||
BTN_STYLUS3 => Some(Trigger::TabletStylusButton3),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(trigger) = trigger {
|
||||
if event.button_state() == ButtonState::Pressed {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
|
||||
if self.niri.mods_with_tablet_stylus_binds.contains(&modifiers) {
|
||||
let bind = {
|
||||
let config = self.niri.config.borrow();
|
||||
let bindings = config.binds.0.iter();
|
||||
find_configured_bind(bindings, mod_key, trigger, mods)
|
||||
}
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
if let Some(bind) = bind {
|
||||
self.niri.suppressed_buttons.insert(button);
|
||||
self.handle_bind(bind.clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tool.button(
|
||||
event.button(),
|
||||
button,
|
||||
event.button_state(),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
event.time_msec(),
|
||||
@@ -4034,6 +4129,7 @@ impl State {
|
||||
fallback_output: Option<&Output>,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
let output = evt.device().output(self);
|
||||
let output = output.filter(|output| self.niri.output_exists(output));
|
||||
let output = output.as_ref().or(fallback_output)?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let transform = output.current_transform();
|
||||
@@ -4068,24 +4164,28 @@ impl State {
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mods = modifiers_from_state(mods);
|
||||
let mod_down = mods.contains(mod_key.to_modifiers());
|
||||
|
||||
if self.niri.screenshot_ui.is_open() {
|
||||
if let Some(output) = under.output.clone() {
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
under.output.as_ref()
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, Some(slot))
|
||||
.pointer_down(output, point, Some(slot), mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
@@ -4104,10 +4204,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
} else if !handle.is_grabbed() {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mods = modifiers_from_state(mods);
|
||||
let mod_down = mods.contains(mod_key.to_modifiers());
|
||||
|
||||
if self.niri.layout.is_overview_open()
|
||||
&& !mod_down
|
||||
&& under.layer.is_none()
|
||||
@@ -4220,16 +4316,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output().cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, Some(slot));
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
@@ -4296,6 +4386,12 @@ impl State {
|
||||
// Null-source DnD: weston-dnd --self-only
|
||||
|| grab.is::<DnDGrab<Self, WlSurface, WlSurface>>()
|
||||
}
|
||||
|
||||
fn grab_can_be_cancelled_with_esc(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
|
||||
let grab = grab.as_any();
|
||||
|
||||
grab.is::<PickWindowGrab>() || grab.is::<PickColorGrab>() || Self::is_dnd_grab(grab)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
@@ -4599,6 +4695,9 @@ fn allowed_during_screenshot(action: &Action) -> bool {
|
||||
| Action::Suspend
|
||||
| Action::PowerOffMonitors
|
||||
| Action::PowerOnMonitors
|
||||
// Intended for binds such as volume up/down, lock the screen, etc.
|
||||
| Action::Spawn(_)
|
||||
| Action::SpawnSh(_)
|
||||
// The screenshot UI can handle these.
|
||||
| Action::MoveColumnLeft
|
||||
| Action::MoveColumnLeftOrToMonitorLeft
|
||||
@@ -4678,7 +4777,11 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let _ = device.config_tap_set_enabled(c.tap);
|
||||
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||
let _ = device.config_dwtp_set_enabled(c.dwtp);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(c.drag_lock);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(if c.drag_lock {
|
||||
input::DragLockState::EnabledTimeout
|
||||
} else {
|
||||
input::DragLockState::Disabled
|
||||
});
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed.0);
|
||||
let _ = device.config_left_handed_set(c.left_handed);
|
||||
@@ -5008,6 +5111,18 @@ pub fn mods_with_finger_scroll_binds(mod_key: ModKey, binds: &Binds) -> HashSet<
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mods_with_tablet_stylus_binds(mod_key: ModKey, binds: &Binds) -> HashSet<Modifiers> {
|
||||
mods_with_binds(
|
||||
mod_key,
|
||||
binds,
|
||||
&[
|
||||
Trigger::TabletStylusButton1,
|
||||
Trigger::TabletStylusButton2,
|
||||
Trigger::TabletStylusButton3,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn grab_allows_hot_corner(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
|
||||
let grab = grab.as_any();
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
windows.sort_unstable_by_key(|a| a.id);
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
@@ -220,7 +220,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
|
||||
let print = |surface: &niri_ipc::LayerSurface| {
|
||||
println!(" Surface:");
|
||||
println!(" Namespace: \"{}\"", &surface.namespace);
|
||||
println!(" Namespace: \"{}\"", surface.namespace);
|
||||
|
||||
let interactivity = match surface.keyboard_interactivity {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
|
||||
@@ -568,6 +568,7 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
max_bpc,
|
||||
} = output;
|
||||
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
@@ -651,6 +652,10 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
println!(" Transform: {transform}");
|
||||
}
|
||||
|
||||
if let Some(max_bpc) = max_bpc {
|
||||
println!(" Max bits per channel: {max_bpc}");
|
||||
}
|
||||
|
||||
println!(" Available modes:");
|
||||
for (idx, mode) in modes.into_iter().enumerate() {
|
||||
let Mode {
|
||||
|
||||
@@ -328,6 +328,6 @@ impl MappedLayer {
|
||||
|
||||
impl Drop for MappedLayer {
|
||||
fn drop(&mut self) {
|
||||
remove_pre_commit_hook(self.surface.wl_surface(), self.pre_commit_hook.clone());
|
||||
remove_pre_commit_hook(self.surface.wl_surface(), &self.pre_commit_hook);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,16 +345,17 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
compute_toplevel_bounds(border_config, self.working_area.size)
|
||||
}
|
||||
|
||||
/// Returns the geometry of the active tile relative to and clamped to the working area.
|
||||
/// Returns the geometry of the active window relative to and clamped to the working area.
|
||||
///
|
||||
/// During animations, assumes the final tile position.
|
||||
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
let (tile, offset) = self.tiles_with_offsets().next()?;
|
||||
|
||||
let tile_size = tile.tile_size();
|
||||
let tile_rect = Rectangle::new(offset, tile_size);
|
||||
let window_pos = offset + tile.window_loc();
|
||||
let window_size = tile.window_size();
|
||||
let window_rect = Rectangle::new(window_pos, window_size);
|
||||
|
||||
self.working_area.intersection(tile_rect)
|
||||
self.working_area.intersection(window_rect)
|
||||
}
|
||||
|
||||
pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
|
||||
@@ -490,6 +491,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
// Now, descendants is in back-to-front order, and repositioning them in the front-to-back
|
||||
// order will preserve the subsequent indices and work out right.
|
||||
let mut idx = idx;
|
||||
#[allow(clippy::explicit_counter_loop)]
|
||||
for descendant_idx in descendants.into_iter().rev() {
|
||||
self.raise_window(descendant_idx, idx);
|
||||
idx += 1;
|
||||
|
||||
@@ -290,6 +290,9 @@ pub trait LayoutElement {
|
||||
Some(requested)
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -297,6 +300,22 @@ pub trait LayoutElement {
|
||||
let _ = value;
|
||||
}
|
||||
|
||||
/// The effective geometry corner radius for this element.
|
||||
///
|
||||
/// Returns zero when the element is in windowed fullscreen, since fullscreen windows have
|
||||
/// square corners.
|
||||
///
|
||||
/// This method only handles windowed fullscreen and not maximized/real fullscreen. This is
|
||||
/// because windowed fullscreen is handled by the element itself, whereas other sizing modes
|
||||
/// are handled externally by the Tile, so the corner radius changes for those modes is also
|
||||
/// handled externally.
|
||||
fn geometry_corner_radius(&self) -> CornerRadius {
|
||||
if self.is_windowed_fullscreen() {
|
||||
return CornerRadius::default();
|
||||
}
|
||||
self.rules().geometry_corner_radius.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_child_of(&self, parent: &Self) -> bool;
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules;
|
||||
@@ -2854,13 +2873,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
ws.scrolling_insert_position(pos_within_workspace)
|
||||
};
|
||||
|
||||
let rules = move_.tile.window().rules();
|
||||
let border_width = move_.tile.effective_border_width().unwrap_or(0.);
|
||||
let corner_radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
});
|
||||
let corner_radius = move_
|
||||
.tile
|
||||
.window()
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32);
|
||||
mon.insert_hint = Some(InsertHint {
|
||||
workspace: insert_ws,
|
||||
position,
|
||||
|
||||
@@ -1342,15 +1342,15 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.clean_up_workspaces();
|
||||
}
|
||||
|
||||
/// Returns the geometry of the active tile relative to and clamped to the output.
|
||||
/// Returns the geometry of the active window relative to and clamped to the output.
|
||||
///
|
||||
/// During animations, assumes the final view position.
|
||||
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
if self.overview_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.active_workspace_ref().active_tile_visual_rectangle()
|
||||
self.active_workspace_ref().active_window_visual_rectangle()
|
||||
}
|
||||
|
||||
fn workspace_size(&self, zoom: f64) -> Size<f64, Logical> {
|
||||
|
||||
@@ -2540,10 +2540,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
Some(hint_area)
|
||||
}
|
||||
|
||||
/// Returns the geometry of the active tile relative to and clamped to the view.
|
||||
/// Returns the geometry of the active window relative to and clamped to the view.
|
||||
///
|
||||
/// During animations, assumes the final view position.
|
||||
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
let col = self.columns.get(self.active_column_idx)?;
|
||||
|
||||
let final_view_offset = self.view_offset.target();
|
||||
@@ -2551,12 +2551,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
let (tile, tile_off) = col.tiles().nth(col.active_tile_idx).unwrap();
|
||||
|
||||
let tile_pos = view_off + tile_off;
|
||||
let tile_size = tile.tile_size();
|
||||
let tile_rect = Rectangle::new(tile_pos, tile_size);
|
||||
let window_pos = view_off + tile_off + tile.window_loc();
|
||||
let window_size = tile.window_size();
|
||||
let window_rect = Rectangle::new(window_pos, window_size);
|
||||
|
||||
let view = Rectangle::from_size(self.view_size);
|
||||
view.intersection(tile_rect)
|
||||
view.intersection(window_rect)
|
||||
}
|
||||
|
||||
pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
|
||||
|
||||
@@ -243,6 +243,10 @@ impl LayoutElement for TestWindow {
|
||||
self.0.requested_size.get()
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
self.0.is_windowed_fullscreen.get()
|
||||
}
|
||||
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
self.0.is_pending_windowed_fullscreen.get()
|
||||
}
|
||||
|
||||
@@ -403,9 +403,9 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
let window_size = self.window_size();
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.fit_to(window_size.w as f32, window_size.h as f32);
|
||||
self.rounded_corner_damage.set_corner_radius(radius);
|
||||
}
|
||||
@@ -473,11 +473,22 @@ impl<W: LayoutElement> Tile<W> {
|
||||
border_window_size.w -= border_width * 2.;
|
||||
border_window_size.h -= border_width * 2.;
|
||||
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
})
|
||||
// FIXME: this takes into account the animation from normal sizing mode to
|
||||
// maximized/fullscreen, but it doesn't take into account the corner radius animation from
|
||||
// the window itself.
|
||||
//
|
||||
// Currently, an easy way to see the problem is to start from a window with a nonzero
|
||||
// radius, then go from windowed fullscreen (that forces 0 radius) to regular fullscreen.
|
||||
// At the start of the animation, windowed fullscreen becomes false, but the window hasn't
|
||||
// animated to the normal fullscreen yet, so the radius here jumps to its nonzero value,
|
||||
// even though it should remain zero throughout.
|
||||
//
|
||||
// Later, when windows get the surface shape protocol with radii, this issue will happen
|
||||
// when that changes between animated commits.
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32)
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
self.border.update_render_elements(
|
||||
border_window_size,
|
||||
@@ -496,9 +507,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let radius = if self.visual_border_width().is_some() {
|
||||
radius
|
||||
} else {
|
||||
rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
self.window
|
||||
.geometry_corner_radius()
|
||||
.scaled_by(1. - expanded_progress as f32)
|
||||
};
|
||||
self.shadow.update_render_elements(
|
||||
@@ -1059,9 +1069,9 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// Clip to geometry including during the fullscreen animation to help with buggy clients
|
||||
// that submit a full-sized buffer before acking the fullscreen state (Firefox).
|
||||
let clip_to_geometry = fullscreen_progress < 1. && rules.clip_to_geometry == Some(true);
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
|
||||
// Popups go on top, whether it's resize or not.
|
||||
@@ -1235,11 +1245,10 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// animated corner radius.
|
||||
if fullscreen_progress < 1. && has_border_shader {
|
||||
let border_width = self.visual_border_width().unwrap_or(0.);
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
})
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32)
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
|
||||
let size = self.fullscreen_backdrop.size();
|
||||
|
||||
@@ -1609,11 +1609,11 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
floating.chain(scrolling)
|
||||
}
|
||||
|
||||
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.active_tile_visual_rectangle()
|
||||
self.floating.active_window_visual_rectangle()
|
||||
} else {
|
||||
self.scrolling.active_tile_visual_rectangle()
|
||||
self.scrolling.active_window_visual_rectangle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.compact()
|
||||
.with_writer(io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
.with_ansi_sanitization(false)
|
||||
.init();
|
||||
|
||||
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||
@@ -171,7 +172,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let display = Display::new().unwrap();
|
||||
|
||||
// Increase the buffer size so that it's harder to crash a frozen client with a 1000 Hz mouse.
|
||||
display.handle().set_default_max_buffer_size(1024 * 1024);
|
||||
set_default_max_buffer_size(&display, 1024 * 1024);
|
||||
|
||||
let mut state = State::new(
|
||||
config,
|
||||
@@ -234,7 +235,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").is_none_or(|x| x != "1") {
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
if let Err(err) = sd_notify::notify(&[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
|
||||
@@ -373,3 +374,37 @@ fn notify_fd() -> anyhow::Result<()> {
|
||||
notif.write_all(b"READY=1\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The wayland-server crate has set_default_max_buffer_size() under a libwayland_1_23 feature, but
|
||||
// this hard-requires libwayland-server >= 1.23 which is not present on e.g. Ubuntu 24.04. Since
|
||||
// calling this is an optional enhancement, do it optionally at runtime.
|
||||
fn set_default_max_buffer_size(display: &Display<State>, size: usize) {
|
||||
use std::ffi::c_void;
|
||||
|
||||
unsafe {
|
||||
// RTLD_NOLOAD ensures we only get a handle to the libwayland-server that wayland-rs has
|
||||
// already loaded into this process, rather than potentially pulling in a different copy.
|
||||
let lib = libc::dlopen(
|
||||
c"libwayland-server.so.0".as_ptr(),
|
||||
libc::RTLD_LAZY | libc::RTLD_NOLOAD,
|
||||
);
|
||||
if lib.is_null() {
|
||||
// It's not really expected that this can happen, maybe if some distro changes the
|
||||
// library name?
|
||||
warn!("cannot set default max buffer size: libwayland-server.so.0 is not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
let sym = libc::dlsym(lib, c"wl_display_set_default_max_buffer_size".as_ptr());
|
||||
if sym.is_null() {
|
||||
// Expected on libwayland-server < 1.23.
|
||||
trace!("wl_display_set_default_max_buffer_size is missing; skipping");
|
||||
} else {
|
||||
let func: unsafe extern "C" fn(*mut c_void, libc::size_t) = std::mem::transmute(sym);
|
||||
let display_ptr = display.handle().backend_handle().display_ptr();
|
||||
func(display_ptr.cast(), size);
|
||||
}
|
||||
|
||||
libc::dlclose(lib);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as
|
||||
use anyhow::{bail, ensure, Context};
|
||||
use calloop::futures::Scheduler;
|
||||
use niri_config::debug::PreviewRender;
|
||||
use niri_config::output::MaxBpc;
|
||||
use niri_config::{
|
||||
Config, FloatOrInt, Key, Modifiers, OutputName, TrackLayout, WarpMouseToFocusMode,
|
||||
WorkspaceReference, Xkb,
|
||||
@@ -110,6 +111,7 @@ use smithay::wayland::viewporter::ViewporterState;
|
||||
use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
|
||||
use smithay::wayland::xdg_activation::XdgActivationState;
|
||||
use smithay::wayland::xdg_foreign::XdgForeignState;
|
||||
use wayland_server::protocol::wl_output::WlOutput;
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
use crate::a11y::A11y;
|
||||
@@ -132,7 +134,7 @@ use crate::input::scroll_swipe_gesture::ScrollSwipeGesture;
|
||||
use crate::input::scroll_tracker::ScrollTracker;
|
||||
use crate::input::{
|
||||
apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_mouse_binds,
|
||||
mods_with_wheel_binds, TabletData,
|
||||
mods_with_tablet_stylus_binds, mods_with_wheel_binds, TabletData,
|
||||
};
|
||||
use crate::ipc::server::IpcServer;
|
||||
use crate::layer::mapped::LayerSurfaceRenderElement;
|
||||
@@ -374,6 +376,7 @@ pub struct Niri {
|
||||
pub horizontal_wheel_tracker: ScrollTracker,
|
||||
pub mods_with_mouse_binds: HashSet<Modifiers>,
|
||||
pub mods_with_wheel_binds: HashSet<Modifiers>,
|
||||
pub mods_with_tablet_stylus_binds: HashSet<Modifiers>,
|
||||
pub vertical_finger_scroll_tracker: ScrollTracker,
|
||||
pub horizontal_finger_scroll_tracker: ScrollTracker,
|
||||
pub mods_with_finger_scroll_binds: HashSet<Modifiers>,
|
||||
@@ -933,7 +936,7 @@ impl State {
|
||||
let monitor = self.niri.layout.monitor_for_output(output).unwrap();
|
||||
|
||||
let mut rv = false;
|
||||
let rect = monitor.active_tile_visual_rectangle();
|
||||
let rect = monitor.active_window_visual_rectangle();
|
||||
|
||||
if let Some(rect) = rect {
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
@@ -990,6 +993,12 @@ impl State {
|
||||
|
||||
pub fn confirm_mru(&mut self) {
|
||||
if let Some(window) = self.niri.close_mru(MruCloseRequest::Confirm) {
|
||||
// focus_window() will warp the cursor to the window only when the keyboard focus is on
|
||||
// the layout. However, right now the keyboard focus is still on the MRU (that we had
|
||||
// just closed) since it's only updated at the end of the event loop cycle. Force-update
|
||||
// the keyboard focus here to make cursor warping work.
|
||||
self.update_keyboard_focus();
|
||||
|
||||
self.focus_window(&window);
|
||||
}
|
||||
}
|
||||
@@ -1375,10 +1384,20 @@ impl State {
|
||||
|
||||
let keymap = std::fs::read_to_string(xkb_file).context("failed to read xkb_file")?;
|
||||
|
||||
let xkb = self.niri.seat.get_keyboard().unwrap();
|
||||
xkb.set_keymap_from_string(self, keymap)
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let num_lock = keyboard.modifier_state().num_lock;
|
||||
|
||||
keyboard
|
||||
.set_keymap_from_string(self, keymap)
|
||||
.context("failed to set keymap")?;
|
||||
|
||||
// Restore num lock to its previous value.
|
||||
let mut mods_state = keyboard.modifier_state();
|
||||
if mods_state.num_lock != num_lock {
|
||||
mods_state.num_lock = num_lock;
|
||||
keyboard.set_modifier_state(mods_state);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1522,6 +1541,8 @@ impl State {
|
||||
.on_hotkey_config_updated(new_mod_key);
|
||||
self.niri.mods_with_mouse_binds = mods_with_mouse_binds(new_mod_key, &config.binds);
|
||||
self.niri.mods_with_wheel_binds = mods_with_wheel_binds(new_mod_key, &config.binds);
|
||||
self.niri.mods_with_tablet_stylus_binds =
|
||||
mods_with_tablet_stylus_binds(new_mod_key, &config.binds);
|
||||
self.niri.mods_with_finger_scroll_binds =
|
||||
mods_with_finger_scroll_binds(new_mod_key, &config.binds);
|
||||
}
|
||||
@@ -1914,6 +1935,7 @@ impl State {
|
||||
None
|
||||
}
|
||||
}
|
||||
niri_ipc::OutputAction::MaxBpc { max_bpc } => config.max_bpc = Some(MaxBpc(max_bpc)),
|
||||
});
|
||||
|
||||
self.reload_output_config();
|
||||
@@ -2394,6 +2416,7 @@ impl Niri {
|
||||
let mods_with_mouse_binds = mods_with_mouse_binds(mod_key, &config_.binds);
|
||||
let mods_with_wheel_binds = mods_with_wheel_binds(mod_key, &config_.binds);
|
||||
let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(mod_key, &config_.binds);
|
||||
let mods_with_tablet_stylus_binds = mods_with_tablet_stylus_binds(mod_key, &config_.binds);
|
||||
|
||||
let screenshot_ui = ScreenshotUi::new(animation_clock.clone(), config.clone());
|
||||
let window_mru_ui = WindowMruUi::new(config.clone());
|
||||
@@ -2575,6 +2598,7 @@ impl Niri {
|
||||
horizontal_wheel_tracker: ScrollTracker::new(120),
|
||||
mods_with_mouse_binds,
|
||||
mods_with_wheel_binds,
|
||||
mods_with_tablet_stylus_binds,
|
||||
|
||||
// 10 is copied from Clutter: DISCRETE_SCROLL_STEP.
|
||||
vertical_finger_scroll_tracker: ScrollTracker::new(10),
|
||||
@@ -2872,6 +2896,20 @@ impl Niri {
|
||||
self.reposition_outputs(Some(&output));
|
||||
}
|
||||
|
||||
pub fn output_exists(&self, output: &Output) -> bool {
|
||||
self.output_state.contains_key(output)
|
||||
}
|
||||
|
||||
/// Converts a `WlOutput` to a corresponding `Output` if it exists.
|
||||
///
|
||||
/// Compared to raw `Output::from_resource`, this method also verifies that the output still
|
||||
/// exists in niri. Right after the output global is disabled, but before it is removed for
|
||||
/// good, `Output::from_resource` will succeed, but since niri already forgot the output,
|
||||
/// accessing it can cause logic bugs.
|
||||
pub fn output_from_resource(&self, wl_output: &WlOutput) -> Option<Output> {
|
||||
Output::from_resource(wl_output).filter(|output| self.output_exists(output))
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, output: &Output) {
|
||||
for layer in layer_map_for_output(output).layers() {
|
||||
layer.layer_surface().send_close();
|
||||
@@ -3580,8 +3618,12 @@ impl Niri {
|
||||
|
||||
pub fn output_for_tablet(&self) -> Option<&Output> {
|
||||
let config = self.config.borrow();
|
||||
let map_to_output = config.input.tablet.map_to_output.as_ref();
|
||||
map_to_output.and_then(|name| self.output_by_name_match(name))
|
||||
if config.input.tablet.map_to_focused_output {
|
||||
self.layout.active_output()
|
||||
} else {
|
||||
let map_to_output = config.input.tablet.map_to_output.as_ref();
|
||||
map_to_output.and_then(|name| self.output_by_name_match(name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_for_touch(&self) -> Option<&Output> {
|
||||
|
||||
@@ -200,7 +200,7 @@ fn refresh_workspace_group(protocol_state: &mut ExtWorkspaceManagerState, output
|
||||
// Send workspace_enter for all existing workspaces on this output.
|
||||
for group in &data.instances {
|
||||
let manager: &ExtWorkspaceManagerV1 = group.data().unwrap();
|
||||
for (_, ws) in protocol_state.workspaces.iter() {
|
||||
for ws in protocol_state.workspaces.values() {
|
||||
if ws.output.as_ref() != Some(output) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::{
|
||||
ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr;
|
||||
use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -12,22 +18,20 @@ use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes,
|
||||
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes
|
||||
};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::window::mapped::MappedId;
|
||||
use crate::utils::with_toplevel_role_and_current;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
const EXT_LIST_VERSION: u32 = 1;
|
||||
const WLR_MANAGEMENT_VERSION: u32 = 3;
|
||||
|
||||
pub struct ForeignToplevelManagerState {
|
||||
display: DisplayHandle,
|
||||
instances: Vec<ZwlrForeignToplevelManagerV1>,
|
||||
ext_list_instances: HashSet<ExtForeignToplevelListV1>,
|
||||
wlr_management_instances: HashSet<ZwlrForeignToplevelManagerV1>,
|
||||
toplevels: HashMap<WlSurface, ToplevelData>,
|
||||
}
|
||||
|
||||
@@ -42,33 +46,45 @@ pub trait ForeignToplevelHandler {
|
||||
}
|
||||
|
||||
struct ToplevelData {
|
||||
identifier: MappedId,
|
||||
title: Option<String>,
|
||||
app_id: Option<String>,
|
||||
states: ArrayVec<u32, 3>,
|
||||
output: Option<Output>,
|
||||
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
|
||||
ext_list_instances: HashSet<ExtForeignToplevelHandleV1>,
|
||||
wlr_management_instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
// FIXME: parent.
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForeignToplevelGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
filter: Arc<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ForeignToplevelManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = ForeignToplevelGlobalData {
|
||||
filter: Box::new(filter),
|
||||
filter: Arc::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
|
||||
display
|
||||
.create_global::<D, ExtForeignToplevelListV1, _>(EXT_LIST_VERSION, global_data.clone());
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(
|
||||
WLR_MANAGEMENT_VERSION,
|
||||
global_data,
|
||||
);
|
||||
Self {
|
||||
display: display.clone(),
|
||||
instances: Vec::new(),
|
||||
ext_list_instances: HashSet::new(),
|
||||
wlr_management_instances: HashSet::new(),
|
||||
toplevels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -85,7 +101,11 @@ pub fn refresh(state: &mut State) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for instance in data.instances.keys() {
|
||||
for instance in data.ext_list_instances.iter() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
for instance in data.wlr_management_instances.keys() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
@@ -107,15 +127,23 @@ pub fn refresh(state: &mut State) {
|
||||
};
|
||||
|
||||
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
|
||||
focused = Some((mapped.window.clone(), output.cloned()));
|
||||
focused = Some((mapped.id(), mapped.window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, role, cur, output, false);
|
||||
refresh_toplevel(
|
||||
protocol_state,
|
||||
wl_surface,
|
||||
mapped.id(),
|
||||
role,
|
||||
cur,
|
||||
output,
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
if let Some((identifier, window, output)) = focused {
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role_and_current(toplevel, |role, cur| {
|
||||
@@ -124,7 +152,15 @@ pub fn refresh(state: &mut State) {
|
||||
return;
|
||||
};
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, role, cur, output.as_ref(), true);
|
||||
refresh_toplevel(
|
||||
protocol_state,
|
||||
wl_surface,
|
||||
identifier,
|
||||
role,
|
||||
cur,
|
||||
output.as_ref(),
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -142,7 +178,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
|
||||
continue;
|
||||
}
|
||||
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
for (instance, outputs) in &mut data.wlr_management_instances {
|
||||
if instance.client().as_ref() != Some(&client) {
|
||||
continue;
|
||||
}
|
||||
@@ -157,6 +193,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
|
||||
fn refresh_toplevel(
|
||||
protocol_state: &mut ForeignToplevelManagerState,
|
||||
wl_surface: &WlSurface,
|
||||
identifier: MappedId,
|
||||
role: &XdgToplevelSurfaceRoleAttributes,
|
||||
current: &ToplevelState,
|
||||
output: Option<&Output>,
|
||||
@@ -201,11 +238,24 @@ fn refresh_toplevel(
|
||||
output_changed = true;
|
||||
}
|
||||
|
||||
let something_changed =
|
||||
let something_changed_for_ext = new_title.is_some() || new_app_id.is_some();
|
||||
let something_changed_for_wlr =
|
||||
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
|
||||
|
||||
if something_changed {
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if something_changed_for_ext {
|
||||
for instance in &data.ext_list_instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
if let Some(new_app_id) = new_app_id {
|
||||
instance.app_id(new_app_id.to_owned());
|
||||
}
|
||||
instance.done();
|
||||
}
|
||||
}
|
||||
|
||||
if something_changed_for_wlr {
|
||||
for (instance, outputs) in &mut data.wlr_management_instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
@@ -232,7 +282,7 @@ fn refresh_toplevel(
|
||||
}
|
||||
}
|
||||
|
||||
for outputs in data.instances.values_mut() {
|
||||
for outputs in data.wlr_management_instances.values_mut() {
|
||||
// Clean up dead wl_outputs.
|
||||
outputs.retain(|x| x.is_alive());
|
||||
}
|
||||
@@ -240,16 +290,24 @@ fn refresh_toplevel(
|
||||
Entry::Vacant(entry) => {
|
||||
// New window, start tracking it.
|
||||
let mut data = ToplevelData {
|
||||
identifier,
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
states,
|
||||
output: output.cloned(),
|
||||
instances: HashMap::new(),
|
||||
ext_list_instances: HashSet::new(),
|
||||
wlr_management_instances: HashMap::new(),
|
||||
};
|
||||
|
||||
for manager in &protocol_state.instances {
|
||||
for manager in &protocol_state.ext_list_instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_instance::<State>(&protocol_state.display, &client, manager);
|
||||
data.add_ext_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
for manager in &protocol_state.wlr_management_instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_wlr_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +317,35 @@ fn refresh_toplevel(
|
||||
}
|
||||
|
||||
impl ToplevelData {
|
||||
fn add_instance<D>(
|
||||
fn add_ext_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: &ExtForeignToplevelListV1,
|
||||
) where
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let toplevel = client
|
||||
.create_resource::<ExtForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
|
||||
.unwrap();
|
||||
manager.toplevel(&toplevel);
|
||||
|
||||
toplevel.identifier(self.identifier.to_protocol_identifier());
|
||||
|
||||
if let Some(title) = &self.title {
|
||||
toplevel.title(title.clone());
|
||||
}
|
||||
if let Some(app_id) = &self.app_id {
|
||||
toplevel.app_id(app_id.clone());
|
||||
}
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.ext_list_instances.insert(toplevel);
|
||||
}
|
||||
|
||||
fn add_wlr_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
@@ -292,7 +378,111 @@ impl ToplevelData {
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.instances.insert(toplevel, outputs);
|
||||
self.wlr_management_instances.insert(toplevel, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData, D>
|
||||
for ForeignToplevelManagerState
|
||||
where
|
||||
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
resource: New<ExtForeignToplevelListV1>,
|
||||
_global_data: &ForeignToplevelGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(resource, ());
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_ext_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.ext_list_instances.insert(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ExtForeignToplevelListV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ExtForeignToplevelListV1,
|
||||
request: <ExtForeignToplevelListV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
ext_foreign_toplevel_list_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
// remove the instance here so we won't send any more events.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.ext_list_instances.remove(resource);
|
||||
}
|
||||
ext_foreign_toplevel_list_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ExtForeignToplevelListV1,
|
||||
_data: &(),
|
||||
) {
|
||||
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.ext_list_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ExtForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_resource: &ExtForeignToplevelHandleV1,
|
||||
request: <ExtForeignToplevelHandleV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
ext_foreign_toplevel_handle_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ExtForeignToplevelHandleV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.ext_list_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,10 +507,10 @@ where
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_instance::<D>(handle, client, &manager);
|
||||
data.add_wlr_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.instances.push(manager);
|
||||
state.wlr_management_instances.insert(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
@@ -346,8 +536,9 @@ where
|
||||
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
// remove the instance here so we won't send any more events.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
state.wlr_management_instances.remove(resource);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -359,8 +550,9 @@ where
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
_data: &(),
|
||||
) {
|
||||
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
state.wlr_management_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +575,7 @@ where
|
||||
let Some((surface, _)) = protocol_state
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, data)| data.instances.contains_key(resource))
|
||||
.find(|(_, data)| data.wlr_management_instances.contains_key(resource))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -422,7 +614,7 @@ where
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.instances.retain(|instance, _| instance != resource);
|
||||
data.wlr_management_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +646,16 @@ fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3>
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
|
||||
@@ -250,7 +250,7 @@ impl OutputManagementManagerState {
|
||||
notify_new_head(self, output, conf);
|
||||
}
|
||||
}
|
||||
for (old, _) in self.current_state.iter() {
|
||||
for old in self.current_state.keys() {
|
||||
if !new_state.contains_key(old) {
|
||||
changed = true;
|
||||
notify_removed_head(&mut self.clients, old);
|
||||
|
||||
@@ -209,7 +209,7 @@ impl Blur {
|
||||
|
||||
let mut fbos = [0; 2];
|
||||
gl.GenFramebuffers(fbos.len() as _, fbos.as_mut_ptr());
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbos[0]);
|
||||
gl.BindFramebuffer(ffi::FRAMEBUFFER, fbos[0]);
|
||||
|
||||
let program = &self.program.0.down;
|
||||
gl.UseProgram(program.program);
|
||||
@@ -244,7 +244,7 @@ impl Blur {
|
||||
|
||||
trace!("drawing down {src} to {dst}");
|
||||
gl.FramebufferTexture2D(
|
||||
ffi::DRAW_FRAMEBUFFER,
|
||||
ffi::FRAMEBUFFER,
|
||||
ffi::COLOR_ATTACHMENT0,
|
||||
ffi::TEXTURE_2D,
|
||||
dst,
|
||||
@@ -307,7 +307,7 @@ impl Blur {
|
||||
|
||||
trace!("drawing up {src} to {dst}");
|
||||
gl.FramebufferTexture2D(
|
||||
ffi::DRAW_FRAMEBUFFER,
|
||||
ffi::FRAMEBUFFER,
|
||||
ffi::COLOR_ATTACHMENT0,
|
||||
ffi::TEXTURE_2D,
|
||||
dst,
|
||||
@@ -333,7 +333,7 @@ impl Blur {
|
||||
|
||||
gl.DisableVertexAttribArray(program.attrib_vert as u32);
|
||||
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, 0);
|
||||
gl.BindFramebuffer(ffi::FRAMEBUFFER, 0);
|
||||
gl.DeleteFramebuffers(fbos.len() as _, fbos.as_ptr());
|
||||
})?;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::buffer_y_inverted;
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{
|
||||
@@ -75,12 +76,18 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
* Mat3::from_cols_array(transform.matrix().as_ref())
|
||||
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
|
||||
|
||||
// FIXME: y_inverted
|
||||
let y_invert = if buffer_y_inverted(self.inner.buffer()).unwrap_or(false) {
|
||||
Mat3::from_scale(Vec2::new(1., -1.))
|
||||
} else {
|
||||
Mat3::IDENTITY
|
||||
};
|
||||
|
||||
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
|
||||
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
|
||||
// Apply viewporter src.
|
||||
* Mat3::from_scale(buf_size / src_size)
|
||||
* Mat3::from_translation(-src_loc / buf_size);
|
||||
* Mat3::from_translation(-src_loc / buf_size)
|
||||
* y_invert;
|
||||
|
||||
let geo_size = (self.geometry.size.w as f32, self.geometry.size.h as f32);
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ impl EffectBuffer {
|
||||
let offscreen = if let Some(offscreen) = &mut self.offscreen {
|
||||
offscreen
|
||||
} else {
|
||||
debug!("creating new offscreen texture: {reason}");
|
||||
trace!("creating new offscreen texture: {reason}");
|
||||
let span = tracy_client::span!("creating effect offscreen texture");
|
||||
span.emit_text(reason);
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ uniform float noise;
|
||||
uniform float saturation;
|
||||
uniform vec4 bg_color;
|
||||
|
||||
// Interleaved Gradient Noise
|
||||
float gradient_noise(vec2 uv) {
|
||||
const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
|
||||
return fract(magic.z * fract(dot(uv, magic.xy)));
|
||||
// Sin-less white noise by David Hoskins (MIT License).
|
||||
// https://www.shadertoy.com/view/4djSRW
|
||||
float hash12(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
vec3 saturate(vec3 color, float sat) {
|
||||
@@ -20,7 +22,7 @@ vec4 postprocess(vec4 color) {
|
||||
|
||||
if (noise > 0.0) {
|
||||
vec2 uv = gl_FragCoord.xy;
|
||||
color.rgb += (gradient_noise(uv) - 0.5) * noise;
|
||||
color.rgb += (hash12(uv) - 0.5) * noise;
|
||||
}
|
||||
|
||||
// Mix bg_color behind the texture (both premultiplied alpha).
|
||||
|
||||
@@ -8,5 +8,6 @@ mod animations;
|
||||
mod floating;
|
||||
mod fullscreen;
|
||||
mod layer_shell;
|
||||
mod remove_output;
|
||||
mod transactions;
|
||||
mod window_opening;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_fullscreen_on_removed_output_does_not_panic() {
|
||||
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);
|
||||
|
||||
// Grab the second output's wl_output proxy on the client side.
|
||||
let wl_output = f.client(id).output("headless-2");
|
||||
|
||||
// Remove the output on the niri side. Its wl_output global is disabled but not yet
|
||||
// destroyed, so the client's wl_output resource is still valid and usable.
|
||||
let output = f.niri_output(2);
|
||||
f.niri().remove_output(&output);
|
||||
|
||||
// Request fullscreen on the now-removed wl_output. niri must not panic.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_fullscreen(Some(&wl_output));
|
||||
f.double_roundtrip(id);
|
||||
}
|
||||
@@ -560,6 +560,9 @@ fn key_name(screen_reader: bool, mod_key: ModKey, key: &Key) -> String {
|
||||
Trigger::TouchpadScrollUp => String::from("Touchpad Scroll Up"),
|
||||
Trigger::TouchpadScrollLeft => String::from("Touchpad Scroll Left"),
|
||||
Trigger::TouchpadScrollRight => String::from("Touchpad Scroll Right"),
|
||||
Trigger::TabletStylusButton1 => String::from("Tablet Stylus Button 1"),
|
||||
Trigger::TabletStylusButton2 => String::from("Tablet Stylus Button 2"),
|
||||
Trigger::TabletStylusButton3 => String::from("Tablet Stylus Button 3"),
|
||||
};
|
||||
name.push_str(&pretty);
|
||||
|
||||
|
||||
@@ -370,11 +370,10 @@ impl Thumbnail {
|
||||
|
||||
// Clip thumbnails to their geometry.
|
||||
let radius = if mapped.sizing_mode().is_normal() {
|
||||
mapped.rules().geometry_corner_radius
|
||||
mapped.geometry_corner_radius()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unwrap_or_default();
|
||||
CornerRadius::default()
|
||||
};
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(ctx.renderer);
|
||||
let clip_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned();
|
||||
|
||||
@@ -799,6 +799,8 @@ impl ScreenshotUi {
|
||||
}
|
||||
|
||||
/// The pointer has moved to `point` relative to the current selection output.
|
||||
///
|
||||
/// The point may be outside output bounds.
|
||||
pub fn pointer_motion(&mut self, point: Point<i32, Physical>, slot: Option<TouchSlot>) {
|
||||
let Self::Open {
|
||||
selection,
|
||||
@@ -838,7 +840,8 @@ impl ScreenshotUi {
|
||||
selection.1 += delta;
|
||||
selection.2 += delta;
|
||||
} else {
|
||||
selection.2 = point;
|
||||
let size = output_data[&selection.0].size;
|
||||
selection.2 = Point::new(point.x.clamp(0, size.w - 1), point.y.clamp(0, size.h - 1));
|
||||
}
|
||||
|
||||
self.update_buffers();
|
||||
@@ -849,6 +852,7 @@ impl ScreenshotUi {
|
||||
output: Output,
|
||||
point: Point<i32, Physical>,
|
||||
slot: Option<TouchSlot>,
|
||||
move_existing: bool,
|
||||
) -> bool {
|
||||
let Self::Open {
|
||||
selection,
|
||||
@@ -883,6 +887,23 @@ impl ScreenshotUi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if move_existing {
|
||||
if output != selection.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
*button = Button::Down {
|
||||
touch_slot: slot,
|
||||
on_capture_button: false,
|
||||
last_pos: (output, point),
|
||||
move_state: Some(MoveState {
|
||||
pointer_offset: point - selection.1,
|
||||
touch_slot: slot,
|
||||
}),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(output_data) = output_data.get(&output) else {
|
||||
return false;
|
||||
};
|
||||
@@ -909,6 +930,11 @@ impl ScreenshotUi {
|
||||
last_pos: (output.clone(), point),
|
||||
move_state: None,
|
||||
};
|
||||
|
||||
let point = Point::new(
|
||||
point.x.clamp(0, output_data.size.w - 1),
|
||||
point.y.clamp(0, output_data.size.h - 1),
|
||||
);
|
||||
*selection = (output, point, point);
|
||||
|
||||
self.update_buffers();
|
||||
@@ -939,15 +965,14 @@ impl ScreenshotUi {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Check if this is a move touch and if so, stop the move.
|
||||
if let Some(state) = move_state {
|
||||
if state.touch_slot.is_some_and(|m_slot| Some(m_slot) == slot) {
|
||||
*move_state = None;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if touch_slot != slot {
|
||||
// This is not our main touch, but it might be the move touch. If so, stop the move.
|
||||
if let Some(state) = move_state {
|
||||
if state.touch_slot.is_some_and(|m_slot| Some(m_slot) == slot) {
|
||||
*move_state = None;
|
||||
}
|
||||
};
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ fn spawn_sync(
|
||||
process.env_remove("RUST_LIB_BACKTRACE");
|
||||
}
|
||||
|
||||
// Remove the systemd NOTIFY_SOCKET variable.
|
||||
process.env_remove("NOTIFY_SOCKET");
|
||||
|
||||
// Set DISPLAY if needed.
|
||||
let display = CHILD_DISPLAY.read().unwrap();
|
||||
if let Some(display) = &*display {
|
||||
|
||||
@@ -216,6 +216,24 @@ impl MappedId {
|
||||
pub fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Converts the ID to a string that can be used as an identifier in
|
||||
/// ext_foreign_toplevel_handle_v1::identifier
|
||||
///
|
||||
/// > An identifier is a string that contains up to 32 printable ASCII bytes.
|
||||
/// > An identifier must not be an empty string.
|
||||
///
|
||||
/// Since the ID is exposed to IPC, it's useful for this conversion to be stable and reversible.
|
||||
/// That way, clients can associate a foreign toplevel handle with an IPC window ID.
|
||||
///
|
||||
/// We use the decimal representation of the ID, which is up to 20 characters long for u64::MAX.
|
||||
/// This is within the 32-character limit, and is nice because it matches up with how `niri msg`
|
||||
/// prints the IDs to the console.
|
||||
///
|
||||
/// This namespace can be extended in the future, with any non-numeric prefix to disambiguate.
|
||||
pub fn to_protocol_identifier(self) -> String {
|
||||
format!("{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Interactive resize state.
|
||||
@@ -486,8 +504,7 @@ impl Mapped {
|
||||
let bbox = self.window.bbox_with_popups().to_physical_precise_up(scale);
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
let rules = self.rules();
|
||||
let radius = rules.geometry_corner_radius.unwrap_or_default();
|
||||
let radius = self.geometry_corner_radius();
|
||||
let window_size = self
|
||||
.size()
|
||||
.to_f64()
|
||||
@@ -598,7 +615,7 @@ impl Mapped {
|
||||
|
||||
impl Drop for Mapped {
|
||||
fn drop(&mut self) {
|
||||
remove_pre_commit_hook(self.toplevel().wl_surface(), self.pre_commit_hook.clone());
|
||||
remove_pre_commit_hook(self.toplevel().wl_surface(), &self.pre_commit_hook);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1293,6 +1310,10 @@ impl LayoutElement for Mapped {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
self.is_windowed_fullscreen
|
||||
}
|
||||
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
self.is_pending_windowed_fullscreen
|
||||
}
|
||||
|
||||