mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 420f64f410 | |||
| 8fd9fb73f2 | |||
| 8d83fbae67 | |||
| 414729dce5 | |||
| dbe79b7873 | |||
| 9438f59e2b | |||
| 719255ac35 | |||
| 8a51935224 | |||
| 8d583fe854 | |||
| 74d2b18603 | |||
| fad02316f1 | |||
| 47385c2ecd | |||
| e430d3ab2b | |||
| e472b5b0f1 | |||
| efb169416d | |||
| 3a3a97ec2a | |||
| e9c182a13c | |||
| cfe059c303 | |||
| bd7c748a4f | |||
| 68bb942d21 | |||
| 04c422e43f | |||
| d09fa2709c | |||
| 2c3315aebb | |||
| 5a45088061 | |||
| 404d6dccc4 | |||
| 084f2cb193 | |||
| 25c88b542f | |||
| 6fc50a1fb8 | |||
| 5e11b96f12 | |||
| 849d26d646 | |||
| 9e5716a9db | |||
| f4ebbc8017 | |||
| ce9dd33213 | |||
| 10995ec62c | |||
| c814c656c5 | |||
| 82d4c7569e | |||
| 4f0db78248 | |||
| 3e250cdc12 | |||
| a1b0bd6d1c | |||
| 892470afd3 | |||
| f1cb02cfab | |||
| 5dc4e83ba7 | |||
| 2b58e03d30 | |||
| d4b4407236 | |||
| 26ff5f4bf1 | |||
| d7905e6b74 | |||
| 71d7fa9a61 | |||
| 707f08559c |
@@ -20,9 +20,6 @@ jobs:
|
||||
|
||||
name: test
|
||||
runs-on: ubuntu-24.04
|
||||
# FIXME: remove once it's available in runs-on.
|
||||
# This is necessary for libwayland-server v1.23.
|
||||
container: ubuntu:26.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -31,8 +28,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y ${{ env.DEPS_APT }}
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -114,9 +111,6 @@ jobs:
|
||||
|
||||
name: randomized and slow tests
|
||||
runs-on: ubuntu-24.04
|
||||
# FIXME: remove once it's available in runs-on.
|
||||
# This is necessary for libwayland-server v1.23.
|
||||
container: ubuntu:26.04
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -133,8 +127,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y ${{ env.DEPS_APT }}
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -236,7 +230,7 @@ jobs:
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
container: fedora:42
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -28,9 +28,12 @@ jobs:
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
# Fail if a match is found (exit code 0)
|
||||
grep --recursive 'Since: next release' docs/wiki && exit 1
|
||||
|
||||
# Fail if grep failed (exit code 2)
|
||||
status=$?
|
||||
if [ $status -ne 1 ]; then exit $status; fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
Generated
+329
-367
File diff suppressed because it is too large
Load Diff
+30
-30
@@ -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,11 @@ 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_unix 0.18 has a regression where it doesn't work in normal configurations.
|
||||
# accesskit 0.21 is its correct dependent version.
|
||||
# https://github.com/niri-wm/niri/issues/3594
|
||||
accesskit = { version = "0.21", optional = true }
|
||||
accesskit_unix = { version = "0.17", optional = true }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.5.0"
|
||||
@@ -66,37 +65,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 +121,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 +149,7 @@ dinit = []
|
||||
|
||||
[lints.clippy]
|
||||
new_without_default = "allow"
|
||||
collapsible_match = "allow"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -165,7 +165,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" },
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
|
||||
Included config files can in turn include more files.
|
||||
All included files are watched for changes, and the config live-reloads when any of them change.
|
||||
|
||||
You can include by filename or path.
|
||||
|
||||
* Relative to the current file: `other.kdl` or `./other.kdl`
|
||||
* By absolute path: `/path/to/file.kdl`
|
||||
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
|
||||
|
||||
Includes work only at the top level of the config:
|
||||
|
||||
```kdl,must-fail
|
||||
@@ -116,7 +122,7 @@ window-rule {
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
@@ -89,6 +89,7 @@ input {
|
||||
tablet {
|
||||
// off
|
||||
map-to-output "eDP-1"
|
||||
// map-to-focused-output
|
||||
// left-handed
|
||||
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
|
||||
}
|
||||
@@ -281,6 +282,10 @@ 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.
|
||||
|
||||
Setting 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`.
|
||||
|
||||
### General Settings
|
||||
|
||||
These settings are not specific to a particular input device.
|
||||
|
||||
@@ -382,7 +382,7 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
|
||||
|
||||
```kdl
|
||||
|
||||
@@ -91,7 +91,7 @@ layer-rule {
|
||||
|
||||
#### `layer`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Matches surfaces on this layer-shell layer.
|
||||
Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
|
||||
@@ -230,7 +230,7 @@ layer-rule {
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override the background effect options for this surface.
|
||||
|
||||
@@ -256,7 +256,7 @@ layer-rule {
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ layout {
|
||||
### `preset-column-widths`
|
||||
|
||||
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (not bound by default) to toggle in reverse.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
|
||||
|
||||
`proportion` sets the width as a fraction of the output width, taking gaps into account.
|
||||
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
|
||||
@@ -229,7 +229,7 @@ layout {
|
||||
|
||||
<sup>Since: 0.1.9</sup>
|
||||
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
|
||||
|
||||
`proportion` sets the height as a fraction of the output height, taking gaps into account.
|
||||
|
||||
@@ -331,13 +331,14 @@ config-notification {
|
||||
|
||||
### `blur`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Blur configuration that affects all background blur.
|
||||
|
||||
See the [window effects page](./Window-Effects.md) for an overview of background effects.
|
||||
|
||||
```kdl
|
||||
// These are the default values:
|
||||
blur {
|
||||
// off
|
||||
passes 3
|
||||
@@ -362,7 +363,7 @@ blur {
|
||||
|
||||
#### `passes` and `offset`
|
||||
|
||||
`passes` contols the number of downsample/upsample passes for dual kawase blur.
|
||||
`passes` controls the number of downsample/upsample passes for dual kawase blur.
|
||||
More passes produce a larger, smoother blur, but cost more GPU resources.
|
||||
|
||||
`offset` is the pixel offset multiplier for each pass.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
+16
-4
@@ -40,6 +40,20 @@ hotkey-overlay {
|
||||
}
|
||||
```
|
||||
|
||||
### How to fix lag on external monitors connected to a hybrid GPU laptop?
|
||||
|
||||
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
|
||||
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
|
||||
|
||||
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
|
||||
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
|
||||
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
|
||||
|
||||
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
|
||||
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
|
||||
|
||||
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
|
||||
|
||||
### How to run X11 apps like Steam or Discord?
|
||||
|
||||
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
|
||||
@@ -66,10 +80,8 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
|
||||
|
||||
### Can I enable blur behind semitransparent windows?
|
||||
|
||||
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/54).
|
||||
|
||||
There's also [a PR](https://github.com/niri-wm/niri/pull/1634) adding blur to niri which you can build and run manually.
|
||||
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
|
||||
<sup>Since: 26.04</sup> Yes.
|
||||
See the [window effects](./Window-Effects.md) wiki page.
|
||||
|
||||
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
You can maximize an individual window via `maximize-window-to-edges`.
|
||||
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
|
||||
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
|
||||
You will still see your bar, but not struts, gaps, or borders.
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ sudo dnf install niri dms
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
|
||||
Arch Linux:
|
||||
```
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
|
||||
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
@@ -147,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
|
||||
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
|
||||
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
|
||||
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
|
||||
@@ -13,7 +13,7 @@ Keep in mind that we update the default config in new releases, so if you have a
|
||||
|
||||
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
|
||||
|
||||
<sup>Since: next release</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
|
||||
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
|
||||
|
||||
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 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.
|
||||
@@ -17,9 +17,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 +62,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+23
-35
@@ -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;
|
||||
|
||||
@@ -769,7 +770,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 +780,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,8 @@ pub struct Tablet {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub map_to_output: Option<String>,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_output: bool,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
}
|
||||
|
||||
|
||||
+19
-3
@@ -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,7 @@ mod tests {
|
||||
|
||||
tablet {
|
||||
map-to-output "eDP-1"
|
||||
map-to-focused-output
|
||||
calibration-matrix 1.0 2.0 3.0 \
|
||||
4.0 5.0 6.0
|
||||
}
|
||||
@@ -1097,6 +1112,7 @@ mod tests {
|
||||
map_to_output: Some(
|
||||
"eDP-1",
|
||||
),
|
||||
map_to_focused_output: true,
|
||||
left_handed: false,
|
||||
},
|
||||
touch: Touch {
|
||||
|
||||
+1
-1
@@ -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"
|
||||
```
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.11.0"
|
||||
//! niri-ipc = "=26.4.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -550,11 +550,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
-4
@@ -3,7 +3,7 @@ use std::thread;
|
||||
|
||||
use accesskit::{
|
||||
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Live, Node, NodeId, Role,
|
||||
Tree, TreeId, TreeUpdate,
|
||||
Tree, TreeUpdate,
|
||||
};
|
||||
use accesskit_unix::Adapter;
|
||||
use calloop::LoopHandle;
|
||||
@@ -220,7 +220,6 @@ impl Niri {
|
||||
let update = TreeUpdate {
|
||||
nodes,
|
||||
tree: None,
|
||||
tree_id: TreeId::ROOT,
|
||||
focus,
|
||||
};
|
||||
|
||||
@@ -247,7 +246,6 @@ impl Niri {
|
||||
let update = TreeUpdate {
|
||||
nodes: vec![(ID_ANNOUNCEMENT, node)],
|
||||
tree: None,
|
||||
tree_id: TreeId::ROOT,
|
||||
focus: self.a11y.focus,
|
||||
};
|
||||
|
||||
@@ -341,7 +339,6 @@ impl Niri {
|
||||
(ID_MRU, mru),
|
||||
],
|
||||
tree: Some(tree),
|
||||
tree_id: TreeId::ROOT,
|
||||
focus,
|
||||
}
|
||||
}
|
||||
|
||||
+53
-33
@@ -97,9 +97,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>>,
|
||||
@@ -441,6 +438,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 +492,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 +500,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 +560,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 +611,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
|
||||
@@ -699,7 +706,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 +823,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();
|
||||
@@ -1419,7 +1436,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 +1466,7 @@ impl Tty {
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
|
||||
DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -2266,22 +2283,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;
|
||||
}
|
||||
|
||||
+44
-1
@@ -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>>,
|
||||
}
|
||||
|
||||
@@ -137,6 +141,7 @@ impl Winit {
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
dmabuf_global: None,
|
||||
ipc_outputs,
|
||||
})
|
||||
}
|
||||
@@ -144,7 +149,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 +172,44 @@ impl Winit {
|
||||
|
||||
niri.update_shaders();
|
||||
|
||||
self.create_dmabuf_global(niri);
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
|
||||
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
|
||||
let default_feedback = || {
|
||||
let display = renderer.egl_context().display();
|
||||
let device =
|
||||
EGLDevice::device_for_display(display).context("error getting EGL device")?;
|
||||
let node = device
|
||||
.try_get_render_node()
|
||||
.context("error getting EGL device render node")?
|
||||
.context("failed to query EGL device render node")?;
|
||||
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
|
||||
.build()
|
||||
.context("error building dmabuf feedback")
|
||||
};
|
||||
|
||||
// Fallback to dmabuf v3 if we failed to build feedback.
|
||||
let dmabuf_global = match default_feedback() {
|
||||
Ok(feedback) => niri
|
||||
.dmabuf_state
|
||||
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
|
||||
Err(err) => {
|
||||
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
niri.dmabuf_state
|
||||
.create_global::<State>(&niri.display_handle, primary_formats)
|
||||
}
|
||||
};
|
||||
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"winit".to_owned()
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ impl CompositorHandler for State {
|
||||
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
|
||||
// subsequent toplevel_destroyed() call.
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,13 +572,13 @@ impl State {
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
remove_pre_commit_hook(surface, &prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
|
||||
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
|
||||
namespace: String,
|
||||
) {
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
self.niri.output_from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
|
||||
+24
-5
@@ -361,7 +361,7 @@ impl DndGrabHandler for State {
|
||||
trace!("dnd dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
@@ -383,10 +383,21 @@ impl DndGrabHandler for State {
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
|
||||
trace!("dnd cancelled");
|
||||
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::niri::Niri {
|
||||
fn on_maybe_dnd_ended(&mut self) {
|
||||
self.layout.dnd_end();
|
||||
self.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +472,7 @@ impl SessionLockHandler for State {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
let Some(output) = self.niri.output_from_resource(&output) else {
|
||||
warn!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
@@ -546,7 +557,9 @@ impl ForeignToplevelHandler for State {
|
||||
{
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if let Some(requested_output) =
|
||||
wl_output.and_then(|o| self.niri.output_from_resource(&o))
|
||||
{
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
@@ -622,6 +635,12 @@ delegate_ext_workspace!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// This can happen if the output was removed before this was called.
|
||||
if !self.niri.output_exists(screencopy.output()) {
|
||||
trace!("screencopy output no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
|
||||
@@ -612,7 +612,7 @@ impl XdgShellHandler for State {
|
||||
toplevel: ToplevelSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
) {
|
||||
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
|
||||
|
||||
if let Some((mapped, current_output)) = self
|
||||
.niri
|
||||
|
||||
+28
-16
@@ -41,6 +41,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,6 +295,7 @@ 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()) {
|
||||
@@ -488,19 +491,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 {
|
||||
@@ -3260,8 +3261,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);
|
||||
|
||||
@@ -4034,6 +4035,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();
|
||||
@@ -4296,6 +4298,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
|
||||
@@ -4678,7 +4686,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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
+37
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-4
@@ -110,6 +110,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;
|
||||
@@ -1375,10 +1376,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(())
|
||||
}
|
||||
|
||||
@@ -2872,6 +2883,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 +3605,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,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,5 +1,6 @@
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::buffer_y_inverted;
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{
|
||||
@@ -75,12 +76,18 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
* Mat3::from_cols_array(transform.matrix().as_ref())
|
||||
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
|
||||
|
||||
// FIXME: y_inverted
|
||||
let y_invert = if buffer_y_inverted(self.inner.buffer()).unwrap_or(false) {
|
||||
Mat3::from_scale(Vec2::new(1., -1.))
|
||||
} else {
|
||||
Mat3::IDENTITY
|
||||
};
|
||||
|
||||
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
|
||||
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
|
||||
// Apply viewporter src.
|
||||
* Mat3::from_scale(buf_size / src_size)
|
||||
* Mat3::from_translation(-src_loc / buf_size);
|
||||
* Mat3::from_translation(-src_loc / buf_size)
|
||||
* y_invert;
|
||||
|
||||
let geo_size = (self.geometry.size.w as f32, self.geometry.size.h as f32);
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ impl EffectBuffer {
|
||||
let offscreen = if let Some(offscreen) = &mut self.offscreen {
|
||||
offscreen
|
||||
} else {
|
||||
debug!("creating new offscreen texture: {reason}");
|
||||
trace!("creating new offscreen texture: {reason}");
|
||||
let span = tracy_client::span!("creating effect offscreen texture");
|
||||
span.emit_text(reason);
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ uniform float noise;
|
||||
uniform float saturation;
|
||||
uniform vec4 bg_color;
|
||||
|
||||
// Interleaved Gradient Noise
|
||||
float gradient_noise(vec2 uv) {
|
||||
const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
|
||||
return fract(magic.z * fract(dot(uv, magic.xy)));
|
||||
// Sin-less white noise by David Hoskins (MIT License).
|
||||
// https://www.shadertoy.com/view/4djSRW
|
||||
float hash12(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
vec3 saturate(vec3 color, float sat) {
|
||||
@@ -20,7 +22,7 @@ vec4 postprocess(vec4 color) {
|
||||
|
||||
if (noise > 0.0) {
|
||||
vec2 uv = gl_FragCoord.xy;
|
||||
color.rgb += (gradient_noise(uv) - 0.5) * noise;
|
||||
color.rgb += (hash12(uv) - 0.5) * noise;
|
||||
}
|
||||
|
||||
// Mix bg_color behind the texture (both premultiplied alpha).
|
||||
|
||||
@@ -8,5 +8,6 @@ mod animations;
|
||||
mod floating;
|
||||
mod fullscreen;
|
||||
mod layer_shell;
|
||||
mod remove_output;
|
||||
mod transactions;
|
||||
mod window_opening;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_fullscreen_on_removed_output_does_not_panic() {
|
||||
let mut f = Fixture::new();
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
let id = f.add_client();
|
||||
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(100, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Grab the second output's wl_output proxy on the client side.
|
||||
let wl_output = f.client(id).output("headless-2");
|
||||
|
||||
// Remove the output on the niri side. Its wl_output global is disabled but not yet
|
||||
// destroyed, so the client's wl_output resource is still valid and usable.
|
||||
let output = f.niri_output(2);
|
||||
f.niri().remove_output(&output);
|
||||
|
||||
// Request fullscreen on the now-removed wl_output. niri must not panic.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_fullscreen(Some(&wl_output));
|
||||
f.double_roundtrip(id);
|
||||
}
|
||||
+3
-4
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user