Compare commits

...

88 Commits

Author SHA1 Message Date
HigherOrderLogic 49fc6117fd nix: bump inputs 2026-06-18 14:11:04 +03:00
HigherOrderLogic 165a6d1ee8 nix: remove rust-overlay input 2026-06-18 14:11:04 +03:00
Ivan Molodetskikh fdb6d85fc7 Upgrade accesskit_unix to 0.22
Seems the regression is fixed so we can now upgrade it.
2026-06-16 08:37:58 +03:00
Ivan Molodetskikh 188c5300f7 wiki: Update dynamic cast target description 2026-06-15 19:47:58 +03:00
11backslashes a4b5539baa wiki: Add instructions for working around JetBrains Idea crash when
opening the settings window under Wayland.
2026-06-15 12:52:03 +03:00
Noratrieb 6f1a2c5f0e Use RUSTFLAGS instead of CARGO_BUILD_RUSTFLAGS in dev shell
`CARGO_BUILD_RUSTFLAGS` has a very low precedence and is overruled by
global `target.<tuple>.rustflags`.
Since these rustflags are very important, ensure that they have a higher
precedence. We could even go for `CARGO_ENCODED_RUSTFLAGS` which has the
highest precedence, but is slightly annoying to construct and probably
not worth it.
2026-06-08 20:06:55 +03:00
Ivan Molodetskikh f717ae030f Add comment to updating kb focus in confirm MRU 2026-06-05 08:28:52 +03:00
NSPC911 f3696081d1 fix: warp mouse when switching between recent windows 2026-06-05 08:28:19 +03:00
J. Adly 4b60cbe537 input: add tablet stylus button triggers and binds (#3745)
* input: add tablet stylus button triggers and binds

add tablet stylus button3 and fix stylus bind event flow

* add missing allowed during screenshot check

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-06-05 05:22:59 +00:00
Ivan Molodetskikh f9f43d826a Remove deprecated keep-max-bpc-unchanged debug flag
It's been a stub for two releases now.
2026-05-29 15:01:50 +03:00
Ivan Molodetskikh 3d49db3870 wiki: Clarify max-bpc docs
As discussed: https://oftc.catirclogs.org/wayland/2026-05-29
2026-05-29 14:59:31 +03:00
Michael Yang 9bd6c2cadd feat: add 10-bit framebuffer pixel format
Enable true 10-bit content on supported outputs (tested).
2026-05-28 19:50:25 +03:00
Michael Yang c5253968b4 feat: add per output max-bpc config and ipc action
List current max-bpc values in outputs.

fix: remove 16 bpc and change default behaviour

feat(ipc): add max_bpc and format to output

fix: bpc on output config change

docs: add bpc to Outputs

feat: use atomic commits for connector properties

fix: drm `value_type` breaking change

fix: minor changes based on PR review

Rename bpc to max-bpc.

Add max-bpc output action.

refactor: add set_connector_properties

Fix niri-config parse test.

fix: bail when outside valid max bpc range
2026-05-28 19:50:25 +03:00
Jakob Hellermann 9a6f31012d Fix clippy lints 2026-05-28 08:08:18 +03:00
KarimAlaswad 4294948cf1 Add XF86AudioPause media key mapping
Some Bluetooth earbuds send alternating XF86AudioPlay/XF86AudioPause
keycodes on the same physical press. Without a binding for XF86AudioPause,
every other press is dropped, making playback toggle unreliable.
2026-05-21 17:47:43 +03:00
bokicoder cd5ac3e5e0 nix: add systemd units to the right location 2026-05-15 18:25:56 +03:00
Ivan Molodetskikh 38191826cb Optimize and move .png files from LFS into repo
We want to include them in the tarballs alongside the wiki. I tried
toggling the include LFS in archives option, but it quickly used up most
of our free LFS traffic. So let's move these files in-repo, they are not
that big.

I optimized the files with:

oxipng -o max --strip safe docs/wiki/img/*.png

Need to rememeber to do that for any new .png files.
2026-05-10 08:04:05 +03:00
HigherOrderLogic 0200670d9e Fix typo in issue type name 2026-05-07 23:30:58 -07:00
HigherOrderLogic 90366886b2 Assign issue with type instead of label 2026-05-07 12:36:33 -07:00
ArtikusHG 56654034e9 Prevent leaving an orphaned shell process when using niri-session 2026-05-05 12:30:34 -07:00
Ivan Molodetskikh 1f07cffa9f Allow spawn and spawn-sh while in the screenshot UI 2026-05-02 17:55:45 +03:00
Ivan Molodetskikh cb3a06cd54 Move existing screenshot selection when holding Mod 2026-05-02 14:14:09 +03:00
Ivan Molodetskikh f115f2e5e7 Clamp pointer to output inside screenshot_ui
- Removes some duplication
- Allows for better handling by screenshot UI itself depending on the
  case
2026-05-02 13:32:31 +03:00
Ivan Molodetskikh 2e07282977 input: Extract mod_down upwards
View diff with whitespace ignored.
2026-05-02 13:31:13 +03:00
Ivan Molodetskikh cba0454c94 input: Filter allowed_during_screenshot() for non-kb binds
Forgot about it for those.
2026-05-02 12:06:52 +03:00
Ivan Molodetskikh 5f6f131b24 Add map-to-focused-window tablet setting 2026-05-01 12:18:00 +03:00
Ivan Molodetskikh adb5b3cd2c input/tablet: Use f64 for the target rectangle 2026-05-01 12:08:33 +03:00
Ivan Molodetskikh 0650e7b640 layout: Change active_tile_visual_rectangle() to window
New functionality needs window specifically, and existing logic will be
fine with window too.
2026-04-30 19:20:50 +03:00
Canmi dd1c3bcb9f fix: link to hyprland scrolling layout docs 2026-04-29 06:10:55 -07:00
Dagmawi Ali e5d463e15b render: fix blur on OpenGL ES 2.0 GPUs 2026-04-29 02:43:46 -07:00
Dimitry Ishenko 26100096e8 Revert "Stop including broken LFS files in source tarball"
This reverts commit d8265ad34e.
2026-04-27 09:03:06 -07:00
Ivan Molodetskikh a85b922919 wiki/security: Add a lock screen section 2026-04-27 00:11:59 +03:00
Ivan Molodetskikh 7d2b620ce9 wiki/security: Mention X11 2026-04-27 00:11:59 +03:00
Nick Janetakis a48f2645d9 Add additional video reference 2026-04-26 07:01:18 -07:00
Ivan Molodetskikh 83e839762f wiki: Document the security model 2026-04-26 15:03:14 +03:00
Ivan Molodetskikh 4c1196f45b wiki/releasing: Add updating wayland.app step 2026-04-26 10:46:13 +03:00
Ivan Molodetskikh 8ed0da44d9 wiki: Document the release process 2026-04-25 14:19:21 +03:00
Ivan Molodetskikh 91be662ac6 Update screenshot in the README 2026-04-25 14:14:21 +03:00
Ivan Molodetskikh 4438aefc8d Update README 2026-04-25 11:19:54 +03:00
Ivan Molodetskikh 6c4dfd7772 rpkg: Update licenses 2026-04-25 11:17:16 +03:00
Ivan Molodetskikh 1ad422f0db wiki: Add screenshot to window effects 2026-04-25 11:16:48 +03:00
Ivan Molodetskikh 8fd9fb73f2 Bump Since on the wiki 2026-04-25 09:16:29 +03:00
Ivan Molodetskikh 8d83fbae67 CI: Update Fedora to 42
41 is EOL
2026-04-25 09:12:26 +03:00
Ivan Molodetskikh 414729dce5 Bump version to 26.04 2026-04-25 09:12:26 +03:00
Ivan Molodetskikh dbe79b7873 CI: Fix next release check in release workflow 2026-04-25 09:12:16 +03:00
Ivan Molodetskikh 9438f59e2b Bump Smithay (last fix was merged) 2026-04-24 18:45:29 +03:00
Ivan Molodetskikh 719255ac35 Bump Smithay to fix GTK 4.23 text-input panic 2026-04-24 18:00:34 +03:00
Ivan Molodetskikh 8a51935224 wiki: Update blur example from foot to alacritty 2026-04-24 15:38:50 +03:00
mgabor3141 8d583fe854 Preserve num lock state when loading custom keymap 2026-04-23 23:52:47 -07:00
Ivan Molodetskikh 74d2b18603 wiki: Mention Mod+M default bind 2026-04-22 13:24:01 +03:00
Ivan Molodetskikh fad02316f1 wiki: Update binds list on Getting Started 2026-04-22 13:22:57 +03:00
Ivan Molodetskikh 47385c2ecd wiki: Add missing Since next release 2026-04-22 13:18:39 +03:00
Uzlkav e430d3ab2b wiki: explicitly mention possible include methods 2026-04-22 13:18:14 +03:00
Ivan Molodetskikh e472b5b0f1 wiki: Fix typos 2026-04-21 22:35:13 +03:00
Ivan Molodetskikh efb169416d wiki: Mention these are the default blur values 2026-04-21 22:35:06 +03:00
Ivan Molodetskikh 3a3a97ec2a Remove wrong return before dlclose() 2026-04-21 17:58:59 +03:00
Ivan Molodetskikh e9c182a13c Make use of new DndGrabHandler::cancelled() 2026-04-20 20:42:21 +03:00
Ivan Molodetskikh cfe059c303 Fix clipped surface + y_invert buffer 2026-04-20 20:38:26 +03:00
Ivan Molodetskikh bd7c748a4f Upgrade Smithay (old laptop screenshot fix) 2026-04-20 20:37:33 +03:00
Ivan Molodetskikh 68bb942d21 Add test for set_fullscreen panic on removed wl_output 2026-04-19 14:35:36 +03:00
jan Lemata 04c422e43f tty: Re-evaluate ignored nodes on udev add events and session resume (#3651)
* tty: Re-evaluate ignored nodes on udev add events

When a DRM device is removed and rescanned (e.g. during a dGPU
suspend/resume cycle), the kernel may assign it a new device ID.
Re-evaluating the config paths specifically on UdevEvent::Added
ensures that symlinks like /dev/dri/by-path/... are resolved to
their new underlying IDs, preventing niri from accidentally
opening an ignored hotplugged device.

This check is restricted to device additions to avoid unnecessary
filesystem I/O on the hot path during bursts of other udev events.

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 08:50:56 +00:00
Ivan Molodetskikh d09fa2709c Guard against removed outputs in several places
Output::from_resource() succeeds even after the global has been disabled
and removed from niri. Clients operating on these disabled outputs could
cause panics in several places because niri assumed the output existed.
2026-04-19 11:19:41 +03:00
urayde 2c3315aebb wiki/getting-started: replace dms-shell references for arch 2026-04-19 10:53:55 +03:00
Indi 5a45088061 config: Print first definition in duplicate bind error message (#3536)
* improve config error messages for duplicate binds

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 07:00:18 +00:00
Ivan Molodetskikh 404d6dccc4 config: Expand ~ to home dir in includes 2026-04-19 09:41:24 +03:00
Ivan Molodetskikh 084f2cb193 tty: Skip initialization if session is inactive 2026-04-19 09:23:30 +03:00
Ivan Molodetskikh 25c88b542f tty: Add primary node first when resuming session 2026-04-19 09:19:58 +03:00
Dubakula Sai Venkata Chaitanya 6fc50a1fb8 Fix input lock when starting compositor while on a different TTY (#3593)
* fix: no longer input locks when TTY is switched before full compositor
start

* reword

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 09:14:38 +03:00
Ivan Molodetskikh 5e11b96f12 backend: Downgrade bind_wl_display() warn! to trace! 2026-04-19 08:43:31 +03:00
Austin Riba 849d26d646 backend/winit: Add DMA-BUF support (#3327)
* backend/winit: DMA-BUF setup for Nvidia support

Creates a dmabuf global in the same manner as the tty backend. This
fixes applications failing to launch on Nvidia when using the winit
backend.

This code is adapted from Smithay's Anvil compositor.
See smithay/anvil/src/winit.rs

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 05:43:11 +00:00
Benjamin Bäumler 9e5716a9db Add tablet option map-to-focused-output 2026-04-18 12:41:08 +03:00
Ivan Molodetskikh f4ebbc8017 wiki: Emphasize non-xray warnings 2026-04-18 12:02:39 +03:00
sodiboo ce9dd33213 protocols: implement ext-foreign-toplevel-list-v1
clean up foreign toplevel destruction: don't `retain` to remove one item

document why we have duplicate destruction logic in foreign toplevel

messy to have two of the same comment but like how else do i make that
information readily available to both

Co-authored-by: HigherOrderLogic <73709188+HigherOrderLogic@users.noreply.github.com>
2026-04-18 09:56:50 +03:00
Ivan Molodetskikh 10995ec62c Fix 1 new Clippy warning
I don't like the other ones
2026-04-18 09:30:28 +03:00
Ivan Molodetskikh c814c656c5 Dlsym set_default_max_buffer_size() to avoid wayland-server 1.23 requirement
Ubuntu 24.04 users rejoice
2026-04-17 16:32:10 +03:00
ArijanJ 82d4c7569e Handle trackball scrolling in overview 2026-04-16 21:17:49 +03:00
Ivan Molodetskikh 4f0db78248 wiki/FAQ: Describe hybrid GPU laptop external monitor lag 2026-04-16 21:09:39 +03:00
Ivan Molodetskikh 3e250cdc12 wiki/FAQ: Blur now exists 2026-04-16 21:09:16 +03:00
Ivan Molodetskikh a1b0bd6d1c Assume square corners for windowed fullscreen windows 2026-04-16 19:47:28 +03:00
Ivan Molodetskikh 892470afd3 Call on_maybe_dnd_ended() in two more places
This is not exhaustive and not a good solution. A proper Smithay
callback is needed: https://github.com/Smithay/smithay/issues/1996
2026-04-16 12:35:35 +03:00
Ivan Molodetskikh f1cb02cfab default-config: Bind Mod+Shift+R to switch-preset-column-width-back by default
Height presets aren't frequently needed in my experience, but switching
preset width back is very useful on 21:9 and wider monitors where you
have many more presets.
2026-04-16 10:03:47 +03:00
Ivan Molodetskikh 5dc4e83ba7 Upgrade dependencies 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh 2b58e03d30 Upgrade tracing-subscriber and disable ANSI sanitization 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh d4b4407236 Implement cancelling DnD with Escape
The last Smithay upgrade includes a fix that lets us do this.
2026-04-16 09:54:53 +03:00
Ivan Molodetskikh 26ff5f4bf1 Upgrade Smithay (fix disappeared connectors, fix DnD hang in Electron) 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh d7905e6b74 Downgrade accesskit_unix to 0.17.0
Workaround for a 0.18 regression where accessibility doesn't work under
normal conditions.

https://github.com/niri-wm/niri/issues/3594

https://github.com/AccessKit/accesskit-c/issues/76
2026-04-16 09:54:48 +03:00
Ivan Molodetskikh 71d7fa9a61 effect_buffer: Change debug! to trace! 2026-04-15 21:35:28 +03:00
Ivan Molodetskikh 707f08559c postprocess: Replace IGN with more white-noise-like noise
IGN gives visible patterns when zooming in or increasing noise amount.
2026-04-15 21:28:22 +03:00
100 changed files with 1994 additions and 1019 deletions
-12
View File
@@ -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'
+1 -1
View File
@@ -2,7 +2,7 @@
name: Bug report
about: Report a bug or a crash
title: ''
labels: bug
type: Bug
assignees: ''
---
+5 -11
View File
@@ -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
+6 -3
View File
@@ -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: |
Generated
+302 -344
View File
File diff suppressed because it is too large Load Diff
+27 -30
View File
@@ -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" },
+12 -7
View File
@@ -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&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
<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/
+2
View File
@@ -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
+2
View File
@@ -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]
+2 -21
View File
@@ -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.
+7 -1
View File
@@ -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`:
+12
View File
@@ -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.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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).
+2 -2
View File
@@ -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.
+3 -2
View File
@@ -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.
+22
View File
@@ -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>
+5 -2
View File
@@ -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).
+146
View File
@@ -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.
+16 -4
View File
@@ -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?
+1 -1
View File
@@ -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.
+4 -8
View File
@@ -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% |
+5 -1
View File
@@ -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.
+3 -3
View File
@@ -53,12 +53,12 @@ It shows up as "niri Dynamic Cast Target" in the screencast window dialog.
![Screencast dialog showing niri Dynamic Cast Target.](https://github.com/user-attachments/assets/e236ce74-98ec-4f3a-a99b-29ac1ff324dd)
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:
+61
View File
@@ -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).
+10 -7
View File
@@ -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.
![Screenshot with blur](./img/blur.png)
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
+2
View File
@@ -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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 9.3 KiB

Generated
+4 -25
View File
@@ -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"
}
}
},
+16 -39
View File
@@ -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;
};
};
}
+1 -1
View File
@@ -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
+32 -35
View File
@@ -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
-4
View File
@@ -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,
+4
View File
@@ -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,
}
+30 -5
View File
@@ -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(
+42
View File
@@ -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 {
+1 -1
View File
@@ -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"
```
+59 -1
View File
@@ -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;
+2 -2
View File
@@ -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
+1 -3
View File
@@ -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
+7 -3
View File
@@ -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; }
+1 -1
View File
@@ -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
+1
View File
@@ -109,6 +109,7 @@ impl Headless {
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
max_bpc: None,
},
);
+196 -89
View File
@@ -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,
+45 -1
View File
@@ -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()
}
+3 -3
View File
@@ -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 -2
View File
@@ -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()
};
+24 -5
View File
@@ -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() {
+1 -1
View File
@@ -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
+330 -215
View File
@@ -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();
+7 -2
View File
@@ -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 {
+1 -1
View File
@@ -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);
}
}
+7 -5
View File
@@ -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;
+24 -6
View File
@@ -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,
+3 -3
View File
@@ -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> {
+6 -6
View File
@@ -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>> {
+4
View File
@@ -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()
}
+28 -19
View File
@@ -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();
+3 -3
View File
@@ -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()
}
}
+37 -2
View File
@@ -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);
}
}
+48 -6
View File
@@ -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> {
+1 -1
View File
@@ -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;
}
+238 -36
View File
@@ -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);
+1 -1
View File
@@ -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);
+4 -4
View File
@@ -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());
})?;
+9 -2
View File
@@ -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);
+1 -1
View File
@@ -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);
+7 -5
View File
@@ -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).
+1
View File
@@ -8,5 +8,6 @@ mod animations;
mod floating;
mod fullscreen;
mod layer_shell;
mod remove_output;
mod transactions;
mod window_opening;
+34
View File
@@ -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);
}
+3
View File
@@ -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);
+3 -4
View File
@@ -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();
+34 -9
View File
@@ -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;
}
+3
View File
@@ -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 {
+24 -3
View File
@@ -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
}