mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e140a32b1 | |||
| f30db163b5 | |||
| a78f07cd58 | |||
| 765a241c5a | |||
| a00b271a15 | |||
| e1015ac92f | |||
| a34ed51586 | |||
| 5ddcf195dd | |||
| e11abe554f | |||
| 9261fd6342 | |||
| fb2f66f361 | |||
| e2e15b7a18 | |||
| 0a416eedda | |||
| d7184a04b9 | |||
| bdf394260a | |||
| 74d14be01f | |||
| 3ccb06f564 | |||
| d9e755d575 | |||
| 87e2dd0361 | |||
| dd93c39ed0 | |||
| 849788bb28 | |||
| 9015ff8e36 | |||
| e546b339a3 | |||
| b39edf405a | |||
| b98f4906da | |||
| e82830c68c | |||
| 238caaf8da | |||
| 9c79108afa | |||
| 2571242887 | |||
| 6f92b3296a | |||
| 570ea119ba | |||
| df4614e62c | |||
| 3672e79369 | |||
| 2d16abdaae | |||
| ff081acddc | |||
| afe27a143b | |||
| fd2916eb72 | |||
| e9d888cd52 | |||
| 05599ce2c4 | |||
| 0fb6c5706b | |||
| 79aaa4c6c0 | |||
| 7e559dc468 | |||
| 45fc763281 | |||
| 39d3cd2415 | |||
| 19b1074a8b | |||
| 539a5a8030 | |||
| 53b7477d20 | |||
| c34f7b18ec | |||
| a6baef7b68 | |||
| 10df9f4717 | |||
| 9f8eadc5bc | |||
| a496307daf | |||
| bc7bb51b6f | |||
| b7eb8a635b | |||
| d060b06667 | |||
| 54c2e2ab47 | |||
| df3f3979e9 | |||
| 6215b5f0b1 | |||
| 3bfa4a71ff | |||
| 3158f5a9c0 | |||
| d8250fa876 | |||
| cf0b4bc0ca | |||
| 1ab1737653 | |||
| b5640d5293 | |||
| 860a08cce6 | |||
| 2a9d0e495a | |||
| 7f132ecf95 | |||
| 1a63089d67 | |||
| 88dc6e22d0 | |||
| ce8171bed3 | |||
| 6edd29170f | |||
| 9d62b94688 | |||
| 4d295418ce | |||
| f01d48bc51 | |||
| 31ca509160 | |||
| 396097c3ab | |||
| ad62c8e487 | |||
| 9e73beb165 | |||
| 4fca614510 | |||
| 19e55a2df0 | |||
| 6472209b45 | |||
| d9ceff7c70 | |||
| 813c5ee05f | |||
| 47e217c00e | |||
| 9b52465e42 | |||
| 7d60231e35 | |||
| 7a237e519c | |||
| c4462d0c7f | |||
| f85cb5c5f9 | |||
| 7ca46b44b2 | |||
| f913219f94 | |||
| 80469abc20 | |||
| 890935d2ba | |||
| d2fa1f54d4 | |||
| 2641356d41 | |||
| 7c0898570c | |||
| d1fc1ab731 | |||
| d9a9e6ddc4 | |||
| 0cb20b55b8 | |||
| 3d2d7b95d9 | |||
| c22d8358c2 | |||
| 4d058e6111 | |||
| 83a733e085 | |||
| ba29735fbb | |||
| 6fc092cc4f | |||
| f874b2fce5 | |||
| 311ca6b5da |
@@ -10,6 +10,13 @@ assignees: ''
|
||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||
|
||||
<!-- Attaching your full niri config can help diagnose the problem. -->
|
||||
<details><summary>Config</summary>
|
||||
|
||||
```kdl
|
||||
insert config here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<!--
|
||||
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
|
||||
|
||||
@@ -13,10 +13,12 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
- uses: dtolnay/rust-toolchain@1.85.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -246,7 +246,6 @@ jobs:
|
||||
- run: cargo build --all
|
||||
|
||||
freebsd:
|
||||
if: false # Waiting for a new version of the pipewire-rs patch.
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
|
||||
@@ -256,28 +255,26 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
# Required for the rust-cache action to work.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Remove man-db triggers to speed up Ubuntu upgrade by a minute or two during vmactions/freebsd-vm action run.
|
||||
- run: |
|
||||
sudo rm /var/lib/dpkg/info/man-db.*
|
||||
|
||||
- name: Build
|
||||
uses: vmactions/freebsd-vm@966989c456d41351f095a421f60e71342d3bce41 # v1.2.1
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
release: "15.0"
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg update -f
|
||||
pkg install -y ${{ env.DEPS_PKG }}
|
||||
run: |
|
||||
curl -o patch-pipewire_init 'https://cgit.freebsd.org/ports/plain/x11-wm/niri/files/patch-pipewire_init?id=f3f7e555b06d9a87d63c047ce3e82e936a11f2fe'
|
||||
curl -o patch-pipewire_init 'https://cgit.freebsd.org/ports/plain/x11-wm/niri/files/patch-pipewire_init?id=cadf6784d264cf780b6e0ad59bd15b831d36cf80'
|
||||
|
||||
export CARGO_HOME="$PWD/cargo-home"
|
||||
|
||||
cargo fetch
|
||||
|
||||
( cd $CARGO_HOME/git/checkouts/pipewire-rs-*/*/; patch -p2 < $CARGO_HOME/../patch-pipewire_init; )
|
||||
( cd $CARGO_HOME/registry/src/index.crates.io-*/; patch -p1 < $CARGO_HOME/../patch-pipewire_init; )
|
||||
|
||||
cargo build \
|
||||
--offline \
|
||||
@@ -296,12 +293,8 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v4
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
uses: cachix/install-nix-action@v31
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix flake check
|
||||
|
||||
@@ -90,6 +90,24 @@ When creating pull requests, please keep the following in mind.
|
||||
- Remember to document new config options on the wiki.
|
||||
- When opening a pull request, ensure "Allow edits from maintainers" is enabled, so I can make final tweaks before merging.
|
||||
|
||||
### How to get your pull request reviewed more quickly
|
||||
|
||||
- Make it small and self-contained. Avoid mixing several unrelated changes in one PR.
|
||||
- Split the PR into small and self-contained commits. This makes it much easier to review.
|
||||
- Discuss new features, options, or behavior changes beforehand; make sure there's consensus about the design.
|
||||
- When creating the pull request, clearly write what it does, what problem it solves, how to test it.
|
||||
- Follow the rest of the advice from this document.
|
||||
|
||||
## AI contributions
|
||||
|
||||
If you use LLMs for your contribution (issue, comment, pull request), then it is *your job* to check and clean up its output, just like with any other tool.
|
||||
*You* have to spend the time doing this.
|
||||
Particularly:
|
||||
|
||||
- If I can tell that a pull request is mostly LLM-generated, then very likely this pull request will take *significantly more time and effort* than usual to review and finish. This is based on my prior review experience. Therefore, I'm not interested in such pull requests—there's always plenty of human-written ones which take priority.
|
||||
- When using an LLM to prepare an issue, the text usually has a lot of unnecessary wording and irrelevant details. Anyone looking at such an issue will quickly lose interest in reading through it (myself certainly). Clean up the text and keep only those details that actually matter.
|
||||
- When using an LLM to comment on an issue, *you* have to verify that the comment makes sense, contributes something useful, and doesn't have unnecessary repetition.
|
||||
|
||||
|
||||
[cosmic-comp]: https://github.com/pop-os/cosmic-comp
|
||||
[anvil]: https://github.com/Smithay/smithay/tree/master/anvil
|
||||
|
||||
Generated
+489
-1003
File diff suppressed because it is too large
Load Diff
+28
-27
@@ -12,21 +12,21 @@ authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.80.1"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bitflags = "2.9.4"
|
||||
clap = { version = "4.5.48", features = ["derive"] }
|
||||
insta = "1.43.2"
|
||||
bitflags = "2.10.0"
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
insta = "1.46.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
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"] }
|
||||
tracy-client = { version = "0.18.3", default-features = false }
|
||||
tracy-client = { version = "0.18.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
@@ -53,38 +53,37 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.21.0", optional = true }
|
||||
accesskit_unix = { version = "0.17.0", optional = true }
|
||||
accesskit = { version = "0.22.0", optional = true }
|
||||
accesskit_unix = { version = "0.18.0", optional = true }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.5.0"
|
||||
async-io = { version = "2.6.0", optional = true }
|
||||
atomic = "0.6.1"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.23.2", features = ["derive"] }
|
||||
bytemuck = { version = "1.24.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.3", features = ["executor", "futures-io", "signals"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.58"
|
||||
clap_complete_nushell = "4.5.8"
|
||||
clap_complete = "4.5.65"
|
||||
clap_complete_nushell = "4.5.10"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.30.8"
|
||||
glam = "0.30.10"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.176"
|
||||
libc = "0.2.180"
|
||||
libdisplay-info = "0.3.0"
|
||||
log = { version = "0.4.28", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
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"
|
||||
pango = { version = "0.20.12", features = ["v1_44"] }
|
||||
pangocairo = "0.20.10"
|
||||
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.0"
|
||||
portable-atomic = { version = "1.11.1", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.17"
|
||||
sd-notify = "0.4.5"
|
||||
serde.workspace = true
|
||||
@@ -93,11 +92,10 @@ smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.7", optional = true }
|
||||
wayland-backend = "0.3.11"
|
||||
wayland-scanner = "0.31.7"
|
||||
wayland-backend = "0.3.12"
|
||||
wayland-scanner = "0.31.8"
|
||||
xcursor = "0.3.10"
|
||||
zbus = { version = "5.11.0", optional = true }
|
||||
zbus = { version = "5.13.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -121,22 +119,25 @@ features = [
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.1"
|
||||
insta.workspace = true
|
||||
proptest = "1.8.0"
|
||||
proptest-derive = { version = "0.6.0", features = ["boxed_union"] }
|
||||
proptest = "1.9.0"
|
||||
proptest-derive = { version = "0.7.0", features = ["boxed_union"] }
|
||||
rayon = "1.11.0"
|
||||
wayland-client = "0.31.11"
|
||||
wayland-client = "0.31.12"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3.32"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, accessibility tree, power button handling).
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url", "dep:accesskit", "dep:accesskit_unix"]
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:accesskit", "dep:accesskit_unix"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
# Enables the Tracy profiler instrumentation.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default", "smithay/tracy_gpu_profiling"]
|
||||
# Enables the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-check-cfg=cfg(have_libinput_plugin_system)");
|
||||
if pkg_config::Config::new()
|
||||
.atleast_version("1.30.0")
|
||||
.probe("libinput")
|
||||
.is_ok()
|
||||
{
|
||||
println!("cargo:rustc-cfg=have_libinput_plugin_system")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,6 +9,6 @@ dependencies = [
|
||||
]
|
||||
|
||||
# for KDL highlighting support
|
||||
# TODO: use the official pygments package once https://github.com/pygments/pygments/pull/2936 is merged
|
||||
# FIXME: use the official pygments package once https://github.com/pygments/pygments/pull/2936 is merged
|
||||
[tool.uv.sources]
|
||||
pygments = { git = "https://github.com/chinatsu/pygments", rev = "0f0b0d4da2839e1285881389155bb4605a0a6dc4" }
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
Electron-based applications can run directly on Wayland, but it's not the default.
|
||||
|
||||
For Electron > 28, you can set an environment variable:
|
||||
For Electron ≥ 39, you can use the command-line flag if the app does not default to Wayland:
|
||||
```
|
||||
--ozone-platform=wayland
|
||||
```
|
||||
|
||||
For Electron < 39, you can set an environment variable:
|
||||
```kdl
|
||||
environment {
|
||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||
}
|
||||
```
|
||||
|
||||
For previous versions, you need to pass command-line flags to the target application:
|
||||
For Electron ≤ 28, you need to pass command-line flags to the target application:
|
||||
```
|
||||
--enable-features=UseOzonePlatform --ozone-platform-hint=auto
|
||||
```
|
||||
@@ -22,6 +27,12 @@ If you're having issues with some VSCode hotkeys, try starting `Xwayland` and se
|
||||
That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance.
|
||||
Apparently, VSCode currently unconditionally queries the X server for a keymap.
|
||||
|
||||
### JetBrains IDEs
|
||||
|
||||
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`.
|
||||
|
||||
### WezTerm
|
||||
|
||||
> [!NOTE]
|
||||
@@ -63,6 +74,9 @@ environment {
|
||||
}
|
||||
```
|
||||
|
||||
Note that the niri environment config does not propagate to apps and shells started by systemd, for example to DankMaterialShell and its application launcher.
|
||||
You can set the variable in your login shell config (i.e. `~/.bash_profile`) instead, though keep in mind that then it will be set for all compositors, not just niri.
|
||||
|
||||
### Fullscreen games
|
||||
|
||||
Some video games, both Linux-native and on Wine, have various issues when using non-stacking desktop environments.
|
||||
|
||||
@@ -17,6 +17,7 @@ debug {
|
||||
disable-cursor-plane
|
||||
disable-direct-scanout
|
||||
restrict-primary-scanout-to-matching-format
|
||||
force-disable-connectors-on-resume
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
ignore-drm-device "/dev/dri/renderD128"
|
||||
ignore-drm-device "/dev/dri/renderD130"
|
||||
@@ -104,6 +105,19 @@ debug {
|
||||
}
|
||||
```
|
||||
|
||||
### `force-disable-connectors-on-resume`
|
||||
|
||||
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
|
||||
This causes a modeset/screen blank on all outputs.
|
||||
|
||||
If niri rendering is corrupted, or monitors don't light up after a TTY switch, you can try this flag.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
force-disable-connectors-on-resume
|
||||
}
|
||||
```
|
||||
|
||||
### `render-drm-device`
|
||||
|
||||
Override the DRM device that niri will use for all rendering.
|
||||
|
||||
@@ -114,6 +114,30 @@ window-rule {
|
||||
}
|
||||
```
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
```kdl,must-fail
|
||||
// Won't fail if this file doesn't exist.
|
||||
include optional=true "optional-config.kdl"
|
||||
|
||||
// Regular include, will fail if the file doesn't exist.
|
||||
include "required-config.kdl"
|
||||
```
|
||||
|
||||
When an optional include file is missing, niri will emit a warning in the logs on every config reload.
|
||||
This reminds you that the file is missing while still loading the config successfully.
|
||||
|
||||
The optional file is still watched for changes, so if you create it later, the config will automatically reload and apply the new settings.
|
||||
|
||||
Note that `optional` only affects whether a missing file causes an error.
|
||||
If the file exists but contains invalid syntax or other errors, those errors will still cause a parsing failure.
|
||||
|
||||
|
||||
### Merging
|
||||
|
||||
Most config sections are merged between includes, meaning that you can set only a few properties, and only those properties will change.
|
||||
|
||||
@@ -382,6 +382,17 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<sup>Since: next release</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
|
||||
binds {
|
||||
// The pointer will be visible on the screenshot
|
||||
// if it's on top of the window.
|
||||
Alt+Print { screenshot-window show-pointer=true; }
|
||||
}
|
||||
```
|
||||
|
||||
#### `toggle-keyboard-shortcuts-inhibit`
|
||||
|
||||
<sup>Since: 25.02</sup>
|
||||
|
||||
@@ -141,6 +141,13 @@ environment {
|
||||
}
|
||||
```
|
||||
|
||||
Note that these variables do not propagate to the systemd global environment, so tools and applications started by systemd do not see them.
|
||||
In particular, if you start a desktop shell like DankMaterialShell through systemd, then use its built-in application launcher, the apps won't see these environment variables.
|
||||
|
||||
If you want all processes to see the environment variables, you can set them in your login shell config instead (i.e. `~/.bash_profile`).
|
||||
The `niri-session` shell script runs through the login shell and imports all environment variables to systemd before starting niri.
|
||||
Keep in mind that all compositors will see variables set in the login shell, not just niri.
|
||||
|
||||
### `cursor`
|
||||
|
||||
Change the theme and size of the cursor as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables.
|
||||
|
||||
@@ -39,6 +39,9 @@ switch-events {
|
||||
These events trigger when a convertible laptop goes into or out of tablet mode.
|
||||
In tablet mode, the keyboard and mouse are usually inaccessible, so you can use these events to activate the on-screen keyboard.
|
||||
|
||||
> [!NOTE]
|
||||
> The commands below are just examples, you will need to provide your own on-screen keyboard, such as [sysboard](https://github.com/System64fumo/sysboard) or [wvkbd](https://github.com/jjsullivan5196/wvkbd).
|
||||
|
||||
```kdl
|
||||
switch-events {
|
||||
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ hotkey-overlay {
|
||||
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
|
||||
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
|
||||
|
||||
Keep in mind that you can run many Electron apps such as VSCode natively on Wayland by passing the right flags, e.g. `code --ozone-platform-hint=auto`
|
||||
Keep in mind that you can run many Electron apps such as VSCode or Discord natively on Wayland by passing the right flags, as described [here](./Application-Issues.md#electron-applications).
|
||||
|
||||
### Why doesn't niri integrate Xwayland like other compositors?
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ systemctl --user add-wants niri.service dms
|
||||
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
|
||||
```
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
|
||||
paru -S dms-shell-bin matugen wl-clipboard cliphist cava qt6-multimedia-ffmpeg
|
||||
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
@@ -29,6 +29,8 @@ Or, if not using a display manager, run `niri-session` on a TTY.
|
||||
The default niri config will run Waybar, so you might get two bars on screen.
|
||||
To fix this, stop Waybar with `pkill waybar` command, then open `~/.config/niri/config.kdl` and delete the `spawn-at-startup "waybar"` line.
|
||||
|
||||
Check the DankMaterialShell's [compositor setup page](https://danklinux.com/docs/dankmaterialshell/compositors#niri-configuration) to learn how to configure DMS-specific binds and other niri integrations.
|
||||
|
||||
## Slower and more considered start
|
||||
|
||||
The easiest way to get niri is to install one of the distribution packages.
|
||||
@@ -223,7 +225,7 @@ This defaults to `/usr/bin/niri`.
|
||||
| `resources/niri.service` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/etc/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/etc/dinit.d/user/` |
|
||||
| `resources/dinit/niri.target` (dinit) | `/etc/dinit.d/user/` |
|
||||
|
||||
[Alacritty]: https://github.com/alacritty/alacritty
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
@@ -26,6 +26,9 @@ Note that if you're using the provided `resources/niri-portals.conf`, you also n
|
||||
|
||||
If you do not want to install `nautilus` (say you use `nemo` instead), you can set `org.freedesktop.impl.portal.FileChooser=gtk;` in `niri-portals.conf` to use the GTK portal for file chooser dialogues.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not set the `GDK_BACKEND` environment variable globally as this will break the screencast portal.
|
||||
|
||||
### Authentication Agent
|
||||
|
||||
Required when apps need to ask for root permissions. Something like `plasma-polkit-agent` works fine. Start it [with systemd](./Example-systemd-Setup.md) or with [`spawn-at-startup`](./Configuration:-Miscellaneous.md#spawn-at-startup).
|
||||
|
||||
@@ -11,7 +11,7 @@ When this file is present, niri *will not* automatically create a config at `~/.
|
||||
|
||||
Keep in mind that we update the default config in new releases, so if you have a custom `/etc/niri/config.kdl`, you likely want to inspect and apply the relevant changes too.
|
||||
|
||||
Splitting the niri config file into multiple files, or includes, are not supported yet.
|
||||
You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
|
||||
### Xwayland
|
||||
|
||||
|
||||
@@ -24,12 +24,31 @@ To do that, put files into the correct directories according to this table.
|
||||
| `resources/niri.service` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
| `resources/dinit/niri.target` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
|
||||
Doing this will make niri appear in GDM and other display managers.
|
||||
|
||||
See the [Integrating niri](./Integrating-niri.md) page for further information on distribution integration.
|
||||
|
||||
### Recommended dependencies
|
||||
|
||||
First of all, make sure niri depends on `libwayland-server`.
|
||||
This library is currently loaded dynamically, so it's not picked up as a dependency at niri build time.
|
||||
|
||||
Then, the following dependencies are optional, but strongly recommended.
|
||||
Set them as automatically-installed optional dependencies, if possible.
|
||||
|
||||
- `xwayland-satellite`: required to run X11 applications (Steam, Discord, etc.).
|
||||
- `xdg-desktop-portal-gnome`: required for screencasting.
|
||||
- `xdg-desktop-portal-gtk`: configured as the fallback portal in `niri-portals.conf`.
|
||||
(This is in general the standard fallback portal that you want installed.)
|
||||
- `gnome-keyring`: configured as the Secret portal provider in `niri-portals.conf`.
|
||||
- Your distro's GPU driver package, such as `mesa-dri-drivers` and `mesa-libEGL`.
|
||||
Working hardware acceleration is required for running niri.
|
||||
- Some notification daemon like `mako`, generally required for apps to work correctly.
|
||||
|
||||
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
|
||||
### Running tests
|
||||
|
||||
A bulk of our tests spawn niri compositor instances and test Wayland clients.
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
--replace-fail 'ExecStart=niri' "ExecStart=$out/bin/niri"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
@@ -148,6 +148,7 @@
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
NIRI_BUILD_COMMIT = self.shortRev;
|
||||
};
|
||||
|
||||
passthru = {
|
||||
|
||||
@@ -9,11 +9,11 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.7.2"
|
||||
csscolorparser = "0.8.1"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
|
||||
regex = "1.11.3"
|
||||
regex = "1.12.2"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
@@ -132,6 +132,7 @@ pub enum Action {
|
||||
),
|
||||
ScreenshotWindow(
|
||||
#[knuffel(property(name = "write-to-disk"), default = true)] bool,
|
||||
#[knuffel(property(name = "show-pointer"), default = false)] bool,
|
||||
// Path; not settable from knuffel
|
||||
Option<String>,
|
||||
),
|
||||
@@ -139,6 +140,7 @@ pub enum Action {
|
||||
ScreenshotWindowById {
|
||||
id: u64,
|
||||
write_to_disk: bool,
|
||||
show_pointer: bool,
|
||||
path: Option<String>,
|
||||
},
|
||||
ToggleKeyboardShortcutsInhibit,
|
||||
@@ -354,6 +356,8 @@ pub enum Action {
|
||||
SetDynamicCastWindowById(u64),
|
||||
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
|
||||
ClearDynamicCastTarget,
|
||||
#[knuffel(skip)]
|
||||
StopCast(u64),
|
||||
ToggleOverview,
|
||||
OpenOverview,
|
||||
CloseOverview,
|
||||
@@ -407,15 +411,18 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::ScreenshotWindow {
|
||||
id: None,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => Self::ScreenshotWindow(write_to_disk, path),
|
||||
} => Self::ScreenshotWindow(write_to_disk, show_pointer, path),
|
||||
niri_ipc::Action::ScreenshotWindow {
|
||||
id: Some(id),
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => Self::ScreenshotWindowById {
|
||||
id,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
},
|
||||
niri_ipc::Action::ToggleKeyboardShortcutsInhibit {} => {
|
||||
@@ -685,6 +692,7 @@ impl From<niri_ipc::Action> for Action {
|
||||
Self::SetDynamicCastMonitor(output)
|
||||
}
|
||||
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
|
||||
niri_ipc::Action::StopCast { session_id } => Self::StopCast(session_id),
|
||||
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
|
||||
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
|
||||
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Debug {
|
||||
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>,
|
||||
pub ignored_drm_devices: Vec<PathBuf>,
|
||||
pub force_pipewire_invalid_modifier: bool,
|
||||
@@ -44,6 +45,8 @@ pub struct DebugPart {
|
||||
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>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub render_drm_device: Option<PathBuf>,
|
||||
#[knuffel(children(name = "ignore-drm-device"), unwrap(argument))]
|
||||
@@ -81,6 +84,7 @@ impl MergeWith<DebugPart> for Debug {
|
||||
disable_direct_scanout,
|
||||
keep_max_bpc_unchanged,
|
||||
restrict_primary_scanout_to_matching_format,
|
||||
force_disable_connectors_on_resume,
|
||||
force_pipewire_invalid_modifier,
|
||||
emulate_zero_presentation_time,
|
||||
disable_resize_throttling,
|
||||
|
||||
+56
-5
@@ -291,7 +291,51 @@ where
|
||||
}
|
||||
|
||||
"include" => {
|
||||
let path: PathBuf = utils::parse_arg_node("include", node, ctx)?;
|
||||
// Parse the path argument
|
||||
let mut iter_args = node.arguments.iter();
|
||||
let path_val = iter_args.next().ok_or_else(|| {
|
||||
DecodeError::missing(
|
||||
node,
|
||||
"additional argument for include path is required",
|
||||
)
|
||||
})?;
|
||||
let path: PathBuf = knuffel::traits::DecodeScalar::decode(path_val, ctx)?;
|
||||
|
||||
// Check for extra arguments
|
||||
if let Some(val) = iter_args.next() {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&val.literal,
|
||||
"argument",
|
||||
"unexpected argument",
|
||||
));
|
||||
}
|
||||
|
||||
// Parse the optional property
|
||||
let mut optional = false;
|
||||
for (name, val) in &node.properties {
|
||||
match &***name {
|
||||
"optional" => {
|
||||
optional = knuffel::traits::DecodeScalar::decode(val, ctx)?;
|
||||
}
|
||||
name_str => {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
name,
|
||||
"property",
|
||||
format!("unexpected property `{}`", name_str.escape_default()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unexpected children
|
||||
for child in node.children() {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
child,
|
||||
"node",
|
||||
format!("unexpected node `{}`", child.node_name.escape_default()),
|
||||
));
|
||||
}
|
||||
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
let path = base.0.join(path);
|
||||
|
||||
@@ -369,10 +413,16 @@ where
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("failed to read included config from {path:?}: {err}"),
|
||||
));
|
||||
if optional && err.kind() == std::io::ErrorKind::NotFound {
|
||||
// Warn about missing optional includes
|
||||
warn!("optional include not found: {path:?}");
|
||||
} else {
|
||||
// Report all other errors normally
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("failed to read included config from {path:?}: {err}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2134,6 +2184,7 @@ mod tests {
|
||||
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(
|
||||
"/dev/dri/renderD129",
|
||||
),
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "1.0.4", optional = true }
|
||||
schemars = { version = "1.2.0", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
+112
-1
@@ -117,6 +117,8 @@ pub enum Request {
|
||||
ReturnError,
|
||||
/// Request information about the overview.
|
||||
OverviewState,
|
||||
/// Request information about screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
/// Reply from niri to client.
|
||||
@@ -161,6 +163,8 @@ pub enum Response {
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
/// Information about the overview.
|
||||
OverviewState(Overview),
|
||||
/// Information about screencasts.
|
||||
Casts(Vec<Cast>),
|
||||
}
|
||||
|
||||
/// Overview information.
|
||||
@@ -264,6 +268,13 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
write_to_disk: bool,
|
||||
|
||||
/// Whether to include the mouse pointer in the screenshot.
|
||||
///
|
||||
/// 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).
|
||||
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = false))]
|
||||
show_pointer: bool,
|
||||
|
||||
/// Path to save the screenshot to.
|
||||
///
|
||||
/// The path must be absolute, otherwise an error is returned.
|
||||
@@ -429,7 +440,7 @@ pub enum Action {
|
||||
},
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn {},
|
||||
/// Expel the focused window from the column.
|
||||
/// Expel the bottom window from the focused column.
|
||||
ExpelWindowFromColumn {},
|
||||
/// Swap focused window with one to the right.
|
||||
SwapWindowRight {},
|
||||
@@ -887,6 +898,16 @@ pub enum Action {
|
||||
},
|
||||
/// Clear the dynamic cast target, making it show nothing.
|
||||
ClearDynamicCastTarget {},
|
||||
/// Stop a PipeWire screencast.
|
||||
///
|
||||
/// wlr-screencopy screencasts cannot currently be stopped via IPC.
|
||||
StopCast {
|
||||
/// Session ID of the screencast to stop.
|
||||
///
|
||||
/// If the session has multiple screencast streams, this will stop all of them.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
session_id: u64,
|
||||
},
|
||||
/// Toggle (open/close) the Overview.
|
||||
ToggleOverview {},
|
||||
/// Open the Overview.
|
||||
@@ -1466,6 +1487,78 @@ pub struct LayerSurface {
|
||||
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// A screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Cast {
|
||||
/// Stream ID of the screencast that uniquely identifies it.
|
||||
pub stream_id: u64,
|
||||
/// Session ID of the screencast.
|
||||
///
|
||||
/// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
|
||||
/// `session_id`. Though, usually there's only one stream per session.
|
||||
///
|
||||
/// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
|
||||
pub session_id: u64,
|
||||
/// Kind of this screencast.
|
||||
pub kind: CastKind,
|
||||
/// Target being captured.
|
||||
pub target: CastTarget,
|
||||
/// Whether this is a Dynamic Cast Target screencast.
|
||||
///
|
||||
/// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
|
||||
///
|
||||
/// Keep in mind that the target can change even if this is `false`.
|
||||
pub is_dynamic_target: bool,
|
||||
/// Whether the cast is currently streaming frames.
|
||||
///
|
||||
/// This can be `false` for example when switching away to a different scene in OBS, which
|
||||
/// pauses the stream.
|
||||
pub is_active: bool,
|
||||
/// Process ID of the screencast consumer, if known.
|
||||
///
|
||||
/// Currently, only wlr-screencopy screencasts can have a pid.
|
||||
pub pid: Option<i32>,
|
||||
/// PipeWire node ID of the screencast stream.
|
||||
///
|
||||
/// This is `None` for wlr-screencopy casts, and also for PipeWire casts before the node is
|
||||
/// created (when the cast is just starting up).
|
||||
pub pw_node_id: Option<u32>,
|
||||
}
|
||||
|
||||
/// Kind of screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum CastKind {
|
||||
/// PipeWire screencast, typically via xdg-desktop-portal-gnome.
|
||||
PipeWire,
|
||||
/// wlr-screencopy protocol screencast.
|
||||
///
|
||||
/// Tools like wf-recorder, and the xdg-desktop-portal-wlr portal.
|
||||
///
|
||||
/// Only wlr-screencopy with damage tracking is reported here. Screencopy without damage is
|
||||
/// treated as a regular screenshot and not reported as a screencast.
|
||||
WlrScreencopy,
|
||||
}
|
||||
|
||||
/// Target of a screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum CastTarget {
|
||||
/// The target is not yet set, or was cleared.
|
||||
Nothing {},
|
||||
/// Casting an output.
|
||||
Output {
|
||||
/// Name of the screencasted output.
|
||||
name: String,
|
||||
},
|
||||
/// Casting a window.
|
||||
Window {
|
||||
/// ID of the screencasted window.
|
||||
id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A compositor event.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1588,6 +1681,24 @@ pub enum Event {
|
||||
/// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
|
||||
path: Option<String>,
|
||||
},
|
||||
/// The screencasts have changed.
|
||||
CastsChanged {
|
||||
/// The new screencast information.
|
||||
///
|
||||
/// This configuration completely replaces the previous configuration. I.e. if any casts
|
||||
/// are missing from here, then they were stopped.
|
||||
casts: Vec<Cast>,
|
||||
},
|
||||
/// A screencast started, or an existing cast changed.
|
||||
CastStartedOrChanged {
|
||||
/// The cast that started or changed.
|
||||
cast: Cast,
|
||||
},
|
||||
/// A screencast stopped.
|
||||
CastStopped {
|
||||
/// Stream ID of the stopped screencast.
|
||||
stream_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Duration> for Timestamp {
|
||||
|
||||
+37
-1
@@ -9,7 +9,7 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
use crate::{Cast, Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
@@ -46,6 +46,9 @@ pub struct EventStreamState {
|
||||
|
||||
/// State of the config.
|
||||
pub config: ConfigState,
|
||||
|
||||
/// State of screencasts.
|
||||
pub casts: CastsState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
@@ -83,6 +86,13 @@ pub struct ConfigState {
|
||||
pub failed: bool,
|
||||
}
|
||||
|
||||
/// The casts state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CastsState {
|
||||
/// Map from a stream id to the screencast.
|
||||
pub casts: HashMap<u64, Cast>,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
@@ -91,6 +101,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events.extend(self.overview.replicate());
|
||||
events.extend(self.config.replicate());
|
||||
events.extend(self.casts.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
@@ -100,6 +111,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
let event = self.overview.apply(event)?;
|
||||
let event = self.config.apply(event)?;
|
||||
let event = self.casts.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
@@ -285,3 +297,27 @@ impl EventStreamStatePart for ConfigState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for CastsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let casts = self.casts.values().cloned().collect();
|
||||
vec![Event::CastsChanged { casts }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::CastsChanged { casts } => {
|
||||
self.casts = casts.into_iter().map(|c| (c.stream_id, c)).collect();
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
self.casts.insert(cast.stream_id, cast);
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
let cast = self.casts.remove(&stream_id);
|
||||
cast.expect("stopped cast was missing from the map");
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.7", package = "gtk4", features = ["v4_12"] }
|
||||
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" }
|
||||
smithay.workspace = true
|
||||
|
||||
@@ -89,11 +89,8 @@ impl TestCase for GradientArea {
|
||||
1.,
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(renderer, g_loc)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
self.border
|
||||
.render(renderer, g_loc, &mut |elem| rv.push(Box::new(elem) as _));
|
||||
|
||||
rv.extend(
|
||||
[BorderRenderElement::new(
|
||||
|
||||
@@ -268,12 +268,14 @@ impl TestCase for Layout {
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout.update_render_elements(Some(&self.output));
|
||||
|
||||
let mut rv = Vec::new();
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, _, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
.render_workspaces(renderer, RenderTarget::Output, true, &mut |elem| {
|
||||
rv.push(Box::new(elem) as _)
|
||||
});
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +119,15 @@ impl TestCase for Tile {
|
||||
true,
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
);
|
||||
self.tile
|
||||
.render(renderer, location, true, RenderTarget::Output)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
|
||||
let mut rv = Vec::new();
|
||||
self.tile.render(
|
||||
renderer,
|
||||
location,
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
&mut |elem| rv.push(Box::new(elem) as _),
|
||||
);
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +52,15 @@ impl TestCase for Window {
|
||||
.to_f64()
|
||||
.downscale(2.);
|
||||
|
||||
self.window
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
1.,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
let mut rv = Vec::new();
|
||||
self.window.render_normal(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
1.,
|
||||
RenderTarget::Output,
|
||||
&mut |elem| rv.push(Box::new(elem) as _),
|
||||
);
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,8 @@ mod imp {
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||
@extends gtk::Widget;
|
||||
@extends gtk::Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
|
||||
@@ -9,7 +9,7 @@ use niri::layout::{
|
||||
use niri::render_helpers::offscreen::OffscreenData;
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
@@ -149,36 +149,30 @@ impl LayoutElement for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
SplitElements {
|
||||
normal: vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
push(
|
||||
SolidColorRenderElement::from_buffer(&inner.buffer, location, alpha, Kind::Unspecified)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location
|
||||
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
popups: vec![],
|
||||
}
|
||||
);
|
||||
push(
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn request_size(
|
||||
|
||||
@@ -85,6 +85,9 @@ BuildRequires: mesa-libEGL
|
||||
Requires: mesa-dri-drivers
|
||||
Requires: mesa-libEGL
|
||||
|
||||
# Loaded through dlopen
|
||||
Requires: libwayland-server
|
||||
|
||||
# Integrated Xwayland support. Not packaged on EPEL
|
||||
%if 0%{?fedora}
|
||||
Requires: xwayland-satellite >= 0.7
|
||||
|
||||
@@ -558,6 +558,12 @@ binds {
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
// While maximize-column leaves gaps and borders around the window,
|
||||
// maximize-window-to-edges doesn't: the window expands to the edges of the screen.
|
||||
// This bind corresponds to normal window maximizing,
|
||||
// e.g. by double-clicking on the titlebar.
|
||||
Mod+M { maximize-window-to-edges; }
|
||||
|
||||
// Expand the focused column to space not taken up by other fully visible columns.
|
||||
// Makes the column "fill the rest of the space".
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
depends-on = dbus
|
||||
after = niri-shutdown
|
||||
chain-to = niri-shutdown
|
||||
options: always-chain
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
ready-notification = pipevar:NOTIFY_FD
|
||||
logfile = $HOME/.local/share/niri/niri.log
|
||||
depends-on: dbus
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -0,0 +1,6 @@
|
||||
type = internal
|
||||
restart = false
|
||||
depends-on: niri
|
||||
waits-for.d: $XDG_CONFIG_HOME/dinit.d/niri.d/
|
||||
waits-for.d: $HOME/.config/dinit.d/niri.d/
|
||||
waits-for.d: /etc/dinit.d/user/niri.d/
|
||||
+26
-2
@@ -59,13 +59,37 @@ elif hash dinitctl >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||
if dinitctl --quiet --user is-started niri 2>/dev/null; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Import the login manager environment into dinit
|
||||
# Might not work correctly for multiline variable names, but
|
||||
# it is reasonable to assume there are none
|
||||
awk 'BEGIN{for(v in ENVIRON) if (v != "AWKPATH" && v != "AWKLIBPATH") print v}' 2>/dev/null | xargs dinitctl --quiet --user setenv 2>/dev/null
|
||||
|
||||
# Usually the dbus service would start as niri's dependency and inherit
|
||||
# environment from dinit, but in case it has already started we need
|
||||
# to update its environment.
|
||||
if hash dbus-update-activation-environment >/dev/null 2>&1; then
|
||||
dbus-update-activation-environment --all >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Create the directory for the logfile, if doesn't exist
|
||||
mkdir --parents $HOME/.local/share/niri
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
dinitctl --quiet --user start niri.target 2>&1
|
||||
|
||||
# Wait for termination
|
||||
dinit-monitor --user --initial -c $'sh -c "
|
||||
if [ "%s" = "stopped" ] || [ "%s" = "failed" ]; then
|
||||
ppid=$(ps -o ppid= -p $$)
|
||||
kill $ppid
|
||||
fi"' niri >/dev/null 2>&1
|
||||
|
||||
# Unset environment that we've set.
|
||||
dinitctl --quiet --user unsetenv WAYLAND_DISPLAY DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET 2>/dev/null
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
fi
|
||||
|
||||
@@ -11,4 +11,4 @@ Before=xdg-desktop-autostart.target
|
||||
[Service]
|
||||
Slice=session.slice
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/niri --session
|
||||
ExecStart=niri --session
|
||||
|
||||
+160
-97
@@ -434,6 +434,7 @@ impl Tty {
|
||||
.unwrap();
|
||||
|
||||
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||
unsafe { init_libinput_plugin_system(&libinput) };
|
||||
{
|
||||
let _span = tracy_client::span!("Libinput::udev_assign_seat");
|
||||
libinput.udev_assign_seat(&seat_name)
|
||||
@@ -646,7 +647,16 @@ impl Tty {
|
||||
|
||||
// It hasn't been removed, update its state as usual.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
if let Err(err) = device.drm.activate(false) {
|
||||
|
||||
// Someone on an old device hit what seems to be a driver bug without this:
|
||||
// https://github.com/YaLTeR/niri/issues/3048
|
||||
let force_disable = self
|
||||
.config
|
||||
.borrow()
|
||||
.debug
|
||||
.force_disable_connectors_on_resume;
|
||||
|
||||
if let Err(err) = device.drm.activate(force_disable) {
|
||||
warn!("error activating DRM device: {err:?}");
|
||||
}
|
||||
if let Some(lease_state) = &mut device.drm_lease_state {
|
||||
@@ -1055,6 +1065,7 @@ impl Tty {
|
||||
if let Err(err) = surface.compositor.reset_state() {
|
||||
warn!("error resetting DrmCompositor state: {err:?}");
|
||||
}
|
||||
surface.compositor.reset_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3324,6 +3335,50 @@ fn make_output_name(
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the libinput plugin system.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function must be called before libinput iterates through the devices, i.e. before
|
||||
/// libinput_udev_assign_seat() or the first call to libinput_path_add_device().
|
||||
unsafe fn init_libinput_plugin_system(libinput: &Libinput) {
|
||||
#[cfg(have_libinput_plugin_system)]
|
||||
unsafe {
|
||||
use std::ffi::{c_char, c_int, CString};
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
use directories::BaseDirs;
|
||||
use input::ffi::libinput;
|
||||
use input::AsRaw as _;
|
||||
|
||||
extern "C" {
|
||||
fn libinput_plugin_system_append_path(libinput: *const libinput, path: *const c_char);
|
||||
fn libinput_plugin_system_append_default_paths(libinput: *const libinput);
|
||||
fn libinput_plugin_system_load_plugins(
|
||||
libinput: *const libinput,
|
||||
flags: c_int,
|
||||
) -> c_int;
|
||||
}
|
||||
const LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE: c_int = 0;
|
||||
let libinput = libinput.as_raw();
|
||||
|
||||
// Also load plugins from $XDG_CONFIG_HOME/libinput/plugins.
|
||||
if let Some(dirs) = BaseDirs::new() {
|
||||
let mut plugins_dir = dirs.config_dir().to_path_buf();
|
||||
plugins_dir.push("libinput");
|
||||
plugins_dir.push("plugins");
|
||||
if let Ok(plugins_dir) = CString::new(plugins_dir.into_os_string().into_vec()) {
|
||||
libinput_plugin_system_append_path(libinput, plugins_dir.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
libinput_plugin_system_append_default_paths(libinput);
|
||||
libinput_plugin_system_load_plugins(libinput, LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
|
||||
}
|
||||
#[cfg(not(have_libinput_plugin_system))]
|
||||
let _ = libinput;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
@@ -3347,30 +3402,32 @@ mod tests {
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@59.96",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
let modeline2 = Modeline {
|
||||
clock: 452.5,
|
||||
hdisplay: 1920,
|
||||
@@ -3384,82 +3441,88 @@ mod tests {
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@143.88",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc_cvt() {
|
||||
// Crosschecked with other calculators like the cvt commandline utility.
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@59.96",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@143.88",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ pub enum Msg {
|
||||
RequestError,
|
||||
/// Print the overview state.
|
||||
OverviewState,
|
||||
/// List screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
@@ -11,6 +11,7 @@ use zbus::{fdo, interface, ObjectServer};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::{CastSessionId, CastStreamId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenCast {
|
||||
@@ -22,7 +23,7 @@ pub struct ScreenCast {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
id: usize,
|
||||
id: CastSessionId,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -30,7 +31,7 @@ pub struct Session {
|
||||
stopped: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CursorMode {
|
||||
#[default]
|
||||
Hidden = 0,
|
||||
@@ -58,12 +59,10 @@ struct RecordWindowProperties {
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
id: CastStreamId,
|
||||
session_id: CastSessionId,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
was_started: Arc<AtomicBool>,
|
||||
@@ -94,14 +93,14 @@ struct StreamParameters {
|
||||
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
session_id: CastSessionId,
|
||||
stream_id: CastStreamId,
|
||||
target: StreamTargetId,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
},
|
||||
StopCast {
|
||||
session_id: usize,
|
||||
session_id: CastSessionId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -118,9 +117,8 @@ impl ScreenCast {
|
||||
));
|
||||
}
|
||||
|
||||
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
||||
let session_id = NUMBER.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{session_id}");
|
||||
let session_id = CastSessionId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
|
||||
@@ -207,8 +205,8 @@ impl Session {
|
||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||
}
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let stream_id = CastStreamId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{}", stream_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
@@ -244,8 +242,8 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(?properties, "record_window");
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let stream_id = CastStreamId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{}", stream_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
@@ -337,7 +335,7 @@ impl Start for ScreenCast {
|
||||
|
||||
impl Session {
|
||||
pub fn new(
|
||||
id: usize,
|
||||
id: CastSessionId,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
@@ -361,8 +359,8 @@ impl Drop for Session {
|
||||
|
||||
impl Stream {
|
||||
fn new(
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
id: CastStreamId,
|
||||
session_id: CastSessionId,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use anyhow::bail;
|
||||
use smithay::reexports::calloop::LoopHandle;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct PipeWire;
|
||||
pub struct Cast;
|
||||
|
||||
impl PipeWire {
|
||||
pub fn new(_event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||
bail!("PipeWire support is disabled (see \"xdp-gnome-screencast\" feature)");
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,6 @@ impl CompositorHandler for State {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend.early_import(surface);
|
||||
|
||||
if is_sync_subsurface(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root_surface = surface.clone();
|
||||
while let Some(parent) = get_parent(&root_surface) {
|
||||
root_surface = parent;
|
||||
@@ -76,6 +72,10 @@ impl CompositorHandler for State {
|
||||
.root_surface
|
||||
.insert(surface.clone(), root_surface.clone());
|
||||
|
||||
if is_sync_subsurface(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
if surface == &root_surface {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
|
||||
+45
-34
@@ -13,16 +13,16 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::dnd::{self, DnDGrab, DndGrabHandler, DndTarget};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, Focus, PointerHandle};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Point, Rectangle};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
@@ -41,8 +41,7 @@ use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
use smithay::wayland::selection::data_device::{
|
||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||
ServerDndGrabHandler,
|
||||
set_data_device_focus, DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::ext_data_control::{
|
||||
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||
@@ -69,7 +68,7 @@ use smithay::{
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
delegate_viewporter, delegate_xdg_activation,
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
@@ -280,7 +279,6 @@ impl KeyboardShortcutsInhibitHandler for State {
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
type SelectionUserData = Arc<[u8]>;
|
||||
@@ -314,23 +312,51 @@ impl DataDeviceHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientDndGrabHandler for State {
|
||||
fn started(
|
||||
impl WaylandDndGrabHandler for State {
|
||||
fn dnd_requested<S: dnd::Source>(
|
||||
&mut self,
|
||||
_source: Option<WlDataSource>,
|
||||
source: S,
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
seat: Seat<Self>,
|
||||
serial: Serial,
|
||||
type_: dnd::GrabType,
|
||||
) {
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon {
|
||||
surface,
|
||||
offset: Point::new(0, 0),
|
||||
});
|
||||
|
||||
match type_ {
|
||||
dnd::GrabType::Pointer => {
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
let start_data = pointer.grab_start_data().unwrap();
|
||||
let grab =
|
||||
DnDGrab::new_pointer(&self.niri.display_handle, start_data, source, seat);
|
||||
pointer.set_grab(self, grab, serial, Focus::Keep);
|
||||
}
|
||||
dnd::GrabType::Touch => {
|
||||
let touch = seat.get_touch().unwrap();
|
||||
let start_data = touch.grab_start_data().unwrap();
|
||||
let grab = DnDGrab::new_touch(&self.niri.display_handle, start_data, source, seat);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
impl DndGrabHandler for State {
|
||||
fn dropped(
|
||||
&mut self,
|
||||
target: Option<DndTarget<'_, Self>>,
|
||||
validated: bool,
|
||||
_seat: Seat<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let target: Option<&WlSurface> = target.map(DndTarget::into_inner);
|
||||
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();
|
||||
@@ -339,7 +365,7 @@ impl ClientDndGrabHandler for State {
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
let mut activate_output = true;
|
||||
if let Some(target) = validated.then_some(target).flatten() {
|
||||
let root = self.niri.find_root_shell_surface(&target);
|
||||
let root = self.niri.find_root_shell_surface(target);
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
@@ -349,19 +375,10 @@ impl ClientDndGrabHandler for State {
|
||||
}
|
||||
|
||||
if activate_output {
|
||||
// Find the output from cursor coordinates.
|
||||
//
|
||||
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
self.niri.layout.focus_output(output);
|
||||
}
|
||||
// Find the output from drop coordinates.
|
||||
if let Some((output, _)) = self.niri.output_under(location) {
|
||||
let output = output.clone();
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +388,6 @@ impl ClientDndGrabHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerDndGrabHandler for State {}
|
||||
|
||||
delegate_data_device!(State);
|
||||
|
||||
impl PrimarySelectionHandler for State {
|
||||
@@ -608,11 +623,7 @@ impl ScreencopyHandler for State {
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
|
||||
trace!("screencopy manager destroyed already");
|
||||
return;
|
||||
};
|
||||
queue.push(screencopy);
|
||||
self.niri.screencopy_state.push(manager, screencopy);
|
||||
} else {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if let Err(err) = self
|
||||
|
||||
+40
-33
@@ -24,7 +24,6 @@ use smithay::wayland::compositor::{
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::{self, Layer};
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
@@ -85,7 +84,7 @@ impl XdgShellHandler for State {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
let is_dnd_grab = Self::is_dnd_grab(grab.as_any());
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
@@ -105,7 +104,7 @@ impl XdgShellHandler for State {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
let is_dnd_grab = Self::is_dnd_grab(grab.as_any());
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
@@ -134,13 +133,13 @@ impl XdgShellHandler for State {
|
||||
|
||||
match &start_data {
|
||||
PointerOrTouchStartData::Pointer(_) => {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None) {
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
}
|
||||
PointerOrTouchStartData::Touch(_) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None) {
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
@@ -268,15 +267,6 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||
// replace each other.
|
||||
// FIXME: do this properly.
|
||||
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||
return;
|
||||
}
|
||||
|
||||
let popup = PopupKind::Xdg(surface);
|
||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||
trace!("ignoring popup grab because no root surface");
|
||||
@@ -374,25 +364,30 @@ impl XdgShellHandler for State {
|
||||
let keyboard = seat.get_keyboard().unwrap();
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
let can_receive_keyboard_focus = self
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.and_then(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
// Smithay cannot do overlapping grabs, so if we have an IME keyboard grab, don't overwrite
|
||||
// it with a popup keyboard grab. This makes the popup menu work in Telegram while an IME
|
||||
// is active (otherwise it hits the grab mismatch check below).
|
||||
//
|
||||
// The second check is for layer surfaces that can't receive keyboard focus, without it
|
||||
// popups don't work properly in Waybar (GTK 3).
|
||||
let can_receive_keyboard_focus = !self.niri.seat.input_method().keyboard_grabbed()
|
||||
&& self
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.and_then(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
||||
&& !(keyboard.has_grab(serial)
|
||||
|| grab
|
||||
.previous_serial()
|
||||
.map_or(true, |s| keyboard.has_grab(s)));
|
||||
|| grab.previous_serial().is_none_or(|s| keyboard.has_grab(s)));
|
||||
let pointer_grab_mismatches = pointer.is_grabbed()
|
||||
&& !(pointer.has_grab(serial)
|
||||
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
||||
|| grab.previous_serial().is_none_or(|s| pointer.has_grab(s)));
|
||||
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
|
||||
trace!("ignoring popup grab because of current grab mismatch");
|
||||
grab.ungrab(PopupUngrabStrategy::All);
|
||||
@@ -1256,7 +1251,7 @@ impl State {
|
||||
let mut target = self.niri.layout.popup_target_rect(window);
|
||||
target.loc -= get_popup_toplevel_coords(popup).to_f64();
|
||||
|
||||
self.position_popup_within_rect(popup, target);
|
||||
self.position_popup_within_rect(popup, target, true);
|
||||
}
|
||||
|
||||
pub fn unconstrain_layer_shell_popup(
|
||||
@@ -1290,14 +1285,26 @@ impl State {
|
||||
target.loc -= layer_geo.loc;
|
||||
target.loc -= get_popup_toplevel_coords(popup);
|
||||
|
||||
self.position_popup_within_rect(popup, target.to_f64());
|
||||
// Don't add padding to layer-shell popups. It's not really needed, and it's unexpected.
|
||||
self.position_popup_within_rect(popup, target.to_f64(), false);
|
||||
}
|
||||
|
||||
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
|
||||
fn position_popup_within_rect(
|
||||
&self,
|
||||
popup: &PopupKind,
|
||||
target: Rectangle<f64, Logical>,
|
||||
padding: bool,
|
||||
) {
|
||||
match popup {
|
||||
PopupKind::Xdg(popup) => {
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = unconstrain_with_padding(state.positioner, target);
|
||||
state.geometry = if padding {
|
||||
unconstrain_with_padding(state.positioner, target)
|
||||
} else {
|
||||
state
|
||||
.positioner
|
||||
.get_unconstrained_geometry(target.to_i32_round())
|
||||
};
|
||||
});
|
||||
}
|
||||
PopupKind::InputMethod(popup) => {
|
||||
@@ -1466,7 +1473,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
|
||||
span.record("serial", format!("{serial:?}"));
|
||||
}
|
||||
|
||||
trace!("taking pending transaction");
|
||||
// trace!("taking pending transaction");
|
||||
if let Some(transaction) = mapped.take_pending_transaction(serial) {
|
||||
// Transaction can be already completed if it ran past the deadline.
|
||||
let disable = state.niri.config.borrow().debug.disable_transactions;
|
||||
|
||||
@@ -4,6 +4,7 @@ use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
@@ -44,6 +45,12 @@ impl NiriInputDevice for WinitVirtualDevice {
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualKeyboard {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
|
||||
+104
-23
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use input::event::gesture::GestureEventCoordinates as _;
|
||||
use niri_config::{
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger,
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger, Xkb,
|
||||
};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
@@ -19,6 +19,7 @@ use smithay::backend::input::{
|
||||
TabletToolTipState, TouchEvent,
|
||||
};
|
||||
use smithay::backend::libinput::LibinputInputBackend;
|
||||
use smithay::input::dnd::DnDGrab;
|
||||
use smithay::input::keyboard::{keysyms, FilterResult, Keysym, Layout, ModifiersState};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent,
|
||||
@@ -31,10 +32,11 @@ use smithay::input::touch::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||
use touch_overview_grab::TouchOverviewGrab;
|
||||
|
||||
@@ -46,10 +48,11 @@ use crate::dbus::freedesktop_a11y::KbMonBlock;
|
||||
use crate::layout::scrolling::ScrollDirection;
|
||||
use crate::layout::{ActivateWindow, LayoutElement as _};
|
||||
use crate::niri::{CastTarget, PointerVisibility, State};
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::ui::mru::{WindowMru, WindowMruUi};
|
||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::spawning::{spawn, spawn_sh};
|
||||
use crate::utils::{center, get_monotonic_time, ResizeEdge};
|
||||
use crate::utils::{center, get_monotonic_time, CastSessionId, ResizeEdge};
|
||||
|
||||
pub mod backend_ext;
|
||||
pub mod move_grab;
|
||||
@@ -358,11 +361,36 @@ impl State {
|
||||
.is_some_and(KeyboardShortcutsInhibitor::is_active)
|
||||
}
|
||||
|
||||
fn on_keyboard<I: InputBackend>(
|
||||
fn on_keyboard<I: InputBackend + 'static>(
|
||||
&mut self,
|
||||
event: I::KeyboardKeyEvent,
|
||||
consumed_by_a11y: &mut bool,
|
||||
) {
|
||||
) where
|
||||
I::Device: 'static,
|
||||
{
|
||||
// Reset the keymap when handling a physical keyboard after a virtual one.
|
||||
if self.niri.reset_keymap {
|
||||
let device = event.device();
|
||||
let is_virtual_keyboard = (&device as &dyn Any)
|
||||
.downcast_ref::<VirtualKeyboard>()
|
||||
.is_some();
|
||||
if !is_virtual_keyboard {
|
||||
self.niri.reset_keymap = false;
|
||||
|
||||
let config = self.niri.config.borrow();
|
||||
let xkb_config = config.input.keyboard.xkb.clone();
|
||||
std::mem::drop(config);
|
||||
|
||||
if xkb_config != Xkb::default() {
|
||||
self.set_xkb_config(xkb_config.to_xkb_config());
|
||||
} else {
|
||||
// Use locale1 settings if xkb config is unset.
|
||||
let xkb = self.niri.xkb_from_locale1.clone().unwrap_or_default();
|
||||
self.set_xkb_config(xkb.to_xkb_config());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -741,7 +769,7 @@ impl State {
|
||||
self.open_screenshot_ui(show_cursor, path);
|
||||
self.niri.cancel_mru();
|
||||
}
|
||||
Action::ScreenshotWindow(write_to_disk, path) => {
|
||||
Action::ScreenshotWindow(write_to_disk, show_pointer, path) => {
|
||||
let focus = self.niri.layout.focus_with_output();
|
||||
if let Some((mapped, output)) = focus {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
@@ -750,6 +778,7 @@ impl State {
|
||||
output,
|
||||
mapped,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
) {
|
||||
warn!("error taking screenshot: {err:?}");
|
||||
@@ -760,6 +789,7 @@ impl State {
|
||||
Action::ScreenshotWindowById {
|
||||
id,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => {
|
||||
let mut windows = self.niri.layout.windows();
|
||||
@@ -772,6 +802,7 @@ impl State {
|
||||
output,
|
||||
mapped,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
) {
|
||||
warn!("error taking screenshot: {err:?}");
|
||||
@@ -2230,13 +2261,15 @@ impl State {
|
||||
Some(name) => self.niri.output_by_name_match(&name),
|
||||
};
|
||||
if let Some(output) = output {
|
||||
let output = output.downgrade();
|
||||
self.set_dynamic_cast_target(CastTarget::Output(output));
|
||||
self.set_dynamic_cast_target(CastTarget::output(output));
|
||||
}
|
||||
}
|
||||
Action::ClearDynamicCastTarget => {
|
||||
self.set_dynamic_cast_target(CastTarget::Nothing);
|
||||
}
|
||||
Action::StopCast(session_id) => {
|
||||
self.niri.stop_cast(CastSessionId::from(session_id));
|
||||
}
|
||||
Action::ToggleOverview => {
|
||||
self.niri.layout.toggle_overview();
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -2454,6 +2487,35 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
// Warp pointer across the screen during the spatial movement grabs.
|
||||
let spatial_grab = pointer.with_grab(|_, grab| {
|
||||
let grab = grab.as_any();
|
||||
if let Some(grab) = grab.downcast_ref::<SpatialMovementGrab>() {
|
||||
if let Some(output) = grab.view_offset_output() {
|
||||
return Some((output.clone(), true));
|
||||
} else if let Some(output) = grab.workspace_switch_output() {
|
||||
return Some((output.clone(), false));
|
||||
}
|
||||
} else if let Some(grab) = grab.downcast_ref::<MoveGrab>() {
|
||||
if let Some(output) = grab.view_offset_output() {
|
||||
return Some((output.clone(), true));
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
if let Some((output, horizontal)) = spatial_grab.flatten() {
|
||||
if let Some(geo) = self.niri.global_space.output_geometry(&output) {
|
||||
let geo = geo.to_f64();
|
||||
if horizontal {
|
||||
new_pos.x = (new_pos.x - geo.loc.x).rem_euclid(geo.size.w) + geo.loc.x;
|
||||
new_pos.y = new_pos.y.clamp(geo.loc.y, geo.loc.y + geo.size.h - 1.);
|
||||
} else {
|
||||
new_pos.x = new_pos.x.clamp(geo.loc.x, geo.loc.x + geo.size.w - 1.);
|
||||
new_pos.y = (new_pos.y - geo.loc.y).rem_euclid(geo.size.h) + geo.loc.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.niri
|
||||
.global_space
|
||||
@@ -2583,10 +2645,9 @@ impl State {
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
pointer.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = pointer
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(new_pos) {
|
||||
let output = output.clone();
|
||||
@@ -2682,10 +2743,9 @@ impl State {
|
||||
self.niri.tablet_cursor_location = None;
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
pointer.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = pointer
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
@@ -2857,8 +2917,22 @@ impl State {
|
||||
location,
|
||||
};
|
||||
let start_data = PointerOrTouchStartData::Pointer(start_data);
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), false) {
|
||||
let icon = CursorIcon::Grabbing;
|
||||
if let Some(grab) =
|
||||
MoveGrab::new(self, start_data, window.clone(), false, Some(icon))
|
||||
{
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
|
||||
// Set the cursor to Grabbing right away for Mod+LMB since it doesn't
|
||||
// do any other gesture.
|
||||
//
|
||||
// In the overview, we click to activate window and close the overview,
|
||||
// in this case setting the cursor right away would be distracting.
|
||||
if !is_overview_open {
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(icon));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3035,7 +3109,7 @@ impl State {
|
||||
pointer
|
||||
.current_focus()
|
||||
.map(|surface| self.niri.find_root_shell_surface(&surface))
|
||||
.map_or(true, |root| {
|
||||
.is_none_or(|root| {
|
||||
!self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
@@ -4106,7 +4180,8 @@ impl State {
|
||||
location: pos,
|
||||
};
|
||||
let start_data = PointerOrTouchStartData::Touch(start_data);
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None)
|
||||
{
|
||||
handle.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
@@ -4197,10 +4272,9 @@ impl State {
|
||||
);
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
handle.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = handle
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
@@ -4241,6 +4315,13 @@ impl State {
|
||||
self.do_action(action, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dnd_grab(grab: &dyn Any) -> bool {
|
||||
// Normal DnD
|
||||
grab.is::<DnDGrab<Self, WlDataSource, WlSurface>>()
|
||||
// Null-source DnD: weston-dnd --self-only
|
||||
|| grab.is::<DnDGrab<Self, WlSurface, WlSurface>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
|
||||
+75
-30
@@ -14,10 +14,11 @@ use smithay::input::touch::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial, SERIAL_COUNTER};
|
||||
|
||||
use crate::input::PointerOrTouchStartData;
|
||||
use crate::niri::State;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerOrTouchStartData<State>,
|
||||
@@ -27,6 +28,12 @@ pub struct MoveGrab {
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
enable_view_offset: bool,
|
||||
move_icon: CursorIcon,
|
||||
|
||||
// Accumulated and applied in frame().
|
||||
new_location: Point<f64, Logical>,
|
||||
event_timestamp: Option<Duration>,
|
||||
relative_delta: Option<Point<f64, Logical>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -42,17 +49,24 @@ impl MoveGrab {
|
||||
start_data: PointerOrTouchStartData<State>,
|
||||
window: Window,
|
||||
enable_view_offset: bool,
|
||||
move_icon: Option<CursorIcon>,
|
||||
) -> Option<Self> {
|
||||
let (output, pos_within_output) = state.niri.output_under(start_data.location())?;
|
||||
let location = start_data.location();
|
||||
let (output, pos_within_output) = state.niri.output_under(location)?;
|
||||
|
||||
Some(Self {
|
||||
last_location: start_data.location(),
|
||||
last_location: location,
|
||||
start_data,
|
||||
start_output: output.clone(),
|
||||
start_pos_within_output: pos_within_output,
|
||||
window,
|
||||
gesture: GestureState::Recognizing,
|
||||
enable_view_offset,
|
||||
// Moving windows by their titlebars uses the default cursor by default.
|
||||
move_icon: move_icon.unwrap_or(CursorIcon::Default),
|
||||
new_location: location,
|
||||
event_timestamp: None,
|
||||
relative_delta: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +74,10 @@ impl MoveGrab {
|
||||
self.gesture == GestureState::Move
|
||||
}
|
||||
|
||||
pub fn view_offset_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::ViewOffset).then_some(&self.start_output)
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, data: &mut State) {
|
||||
let layout = &mut data.niri.layout;
|
||||
match self.gesture {
|
||||
@@ -112,7 +130,7 @@ impl MoveGrab {
|
||||
if self.start_data.is_pointer() {
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
.set_cursor_image(CursorImageStatus::Named(self.move_icon));
|
||||
}
|
||||
|
||||
true
|
||||
@@ -120,19 +138,25 @@ impl MoveGrab {
|
||||
|
||||
fn begin_view_offset(&mut self, data: &mut State) -> bool {
|
||||
let layout = &mut data.niri.layout;
|
||||
let Some((output, ws_idx)) = layout.workspaces().find_map(|(mon, ws_idx, ws)| {
|
||||
let Some(ws_idx) = layout.workspaces().find_map(|(mon, ws_idx, ws)| {
|
||||
let ws_idx = ws
|
||||
.windows()
|
||||
.any(|w| w.window == self.window)
|
||||
.then_some(ws_idx)?;
|
||||
let output = mon?.output().clone();
|
||||
Some((output, ws_idx))
|
||||
let output = mon?.output();
|
||||
|
||||
// If the window moved to a different output, don't start the gesture.
|
||||
if *output != self.start_output {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ws_idx)
|
||||
}) else {
|
||||
// Can no longer start the gesture.
|
||||
return false;
|
||||
};
|
||||
|
||||
layout.view_offset_gesture_begin(&output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_begin(&self.start_output, Some(ws_idx), false);
|
||||
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
|
||||
@@ -145,14 +169,14 @@ impl MoveGrab {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
location: Point<f64, Logical>,
|
||||
timestamp: Duration,
|
||||
) -> bool {
|
||||
let mut delta = location - self.last_location;
|
||||
self.last_location = location;
|
||||
fn on_frame(&mut self, data: &mut State) -> bool {
|
||||
let Some(timestamp) = self.event_timestamp.take() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let mut delta = self.new_location - self.last_location;
|
||||
let mut relative_delta = self.relative_delta.take().unwrap_or(delta);
|
||||
self.last_location = self.new_location;
|
||||
|
||||
// Try to recognize the gesture.
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
@@ -162,7 +186,7 @@ impl MoveGrab {
|
||||
}
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
let c = location - self.start_data.location();
|
||||
let c = self.new_location - self.start_data.location();
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
let is_floating = data
|
||||
.niri
|
||||
@@ -189,6 +213,7 @@ impl MoveGrab {
|
||||
|
||||
// Apply the whole delta that accumulated during recognizing.
|
||||
delta = c;
|
||||
relative_delta = c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +226,8 @@ impl MoveGrab {
|
||||
};
|
||||
let output = output.clone();
|
||||
|
||||
// Interactive move always uses absolute delta since the window must remain pinned
|
||||
// to the cursor even when it's clamped to monitor bounds.
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
delta,
|
||||
@@ -214,10 +241,11 @@ impl MoveGrab {
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
let res = data
|
||||
.niri
|
||||
.layout
|
||||
.view_offset_gesture_update(-delta.x, timestamp, false);
|
||||
let res = data.niri.layout.view_offset_gesture_update(
|
||||
-relative_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
@@ -277,10 +305,11 @@ impl PointerGrab<State> for MoveGrab {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if !self.on_motion(data, event.location, timestamp) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
self.new_location = event.location;
|
||||
|
||||
// Relative motion takes precedence over normal motion.
|
||||
if self.relative_delta.is_none() {
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +322,9 @@ impl PointerGrab<State> for MoveGrab {
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
|
||||
*self.relative_delta.get_or_insert_default() += event.delta;
|
||||
self.event_timestamp = Some(Duration::from_micros(event.utime));
|
||||
}
|
||||
|
||||
fn button(
|
||||
@@ -337,6 +369,17 @@ impl PointerGrab<State> for MoveGrab {
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(
|
||||
self,
|
||||
data,
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
get_monotonic_time().as_millis() as u32,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
@@ -468,15 +511,17 @@ impl TouchGrab<State> for MoveGrab {
|
||||
return;
|
||||
}
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if !self.on_motion(data, event.location, timestamp) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
self.new_location = event.location;
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
|
||||
@@ -2,6 +2,7 @@ use niri_ipc::PickedColor;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::ExportMem as _;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
@@ -12,7 +13,7 @@ use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::{render_to_vec, RenderTarget};
|
||||
use crate::render_helpers::{render_and_download, RenderTarget};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
@@ -56,7 +57,7 @@ impl PickColorGrab {
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
let pixels = match render_to_vec(
|
||||
let mapping = match render_and_download(
|
||||
renderer,
|
||||
size,
|
||||
scale,
|
||||
@@ -67,6 +68,10 @@ impl PickColorGrab {
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
}),
|
||||
) {
|
||||
Ok(mapping) => mapping,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let pixels = match renderer.map_texture(&mapping) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
@@ -8,10 +8,11 @@ use smithay::input::pointer::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
@@ -19,9 +20,14 @@ pub struct SpatialMovementGrab {
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
|
||||
// Accumulated and applied in frame().
|
||||
new_location: Point<f64, Logical>,
|
||||
event_timestamp: Option<Duration>,
|
||||
relative_delta: Option<Point<f64, Logical>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
@@ -35,6 +41,7 @@ impl SpatialMovementGrab {
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let location = start_data.location;
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
@@ -42,52 +49,40 @@ impl SpatialMovementGrab {
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
last_location: location,
|
||||
start_data,
|
||||
output,
|
||||
workspace_id,
|
||||
gesture,
|
||||
new_location: location,
|
||||
event_timestamp: None,
|
||||
relative_delta: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
pub fn view_offset_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::ViewOffset).then_some(&self.output)
|
||||
}
|
||||
|
||||
pub fn workspace_switch_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::WorkspaceSwitch).then_some(&self.output)
|
||||
}
|
||||
|
||||
fn on_frame(&mut self, data: &mut State) -> bool {
|
||||
let Some(timestamp) = self.event_timestamp.take() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let delta = self
|
||||
.relative_delta
|
||||
.take()
|
||||
.unwrap_or(self.new_location - self.last_location);
|
||||
self.last_location = self.new_location;
|
||||
|
||||
let layout = &mut data.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
let c = event.location - self.start_data.location;
|
||||
let c = self.new_location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
@@ -124,9 +119,47 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
self.new_location = event.location;
|
||||
|
||||
// Relative motion takes precedence over normal motion.
|
||||
if self.relative_delta.is_none() {
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +172,9 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
|
||||
*self.relative_delta.get_or_insert_default() += event.delta;
|
||||
self.event_timestamp = Some(Duration::from_micros(event.utime));
|
||||
}
|
||||
|
||||
fn button(
|
||||
@@ -166,6 +202,17 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(
|
||||
self,
|
||||
data,
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
get_monotonic_time().as_millis() as u32,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
|
||||
+70
-2
@@ -7,8 +7,8 @@ use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Action, Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview,
|
||||
Request, Response, Transform, Window, WindowLayout,
|
||||
Action, Cast, CastKind, CastTarget, Event, KeyboardLayouts, LogicalOutput, Mode, Output,
|
||||
OutputConfigChanged, Overview, Request, Response, Transform, Window, WindowLayout,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
Msg::OverviewState => Request::OverviewState,
|
||||
Msg::Casts => Request::Casts,
|
||||
};
|
||||
|
||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
@@ -496,6 +497,15 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let description = parts.join(" and ");
|
||||
println!("Screenshot captured: {description}");
|
||||
}
|
||||
Event::CastsChanged { casts } => {
|
||||
println!("Casts changed: {casts:?}");
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
println!("Cast started or changed: {cast:?}");
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
println!("Cast stopped: stream id {stream_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,6 +528,28 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("Overview is closed.");
|
||||
}
|
||||
}
|
||||
Msg::Casts => {
|
||||
let Response::Casts(mut casts) = response else {
|
||||
bail!("unexpected response: expected Casts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let casts = serde_json::to_string(&casts).context("error formatting response")?;
|
||||
println!("{casts}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if casts.is_empty() {
|
||||
println!("No screencasts.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
casts.sort_by_key(|c| (c.session_id, c.stream_id));
|
||||
for cast in casts {
|
||||
print_cast(&cast);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -706,6 +738,42 @@ fn print_window(window: &Window) {
|
||||
);
|
||||
}
|
||||
|
||||
fn print_cast(cast: &Cast) {
|
||||
let active = if cast.is_active { "" } else { " (inactive)" };
|
||||
println!("Cast stream ID {}:{active}", cast.stream_id);
|
||||
println!(" Session ID: {}", cast.session_id);
|
||||
|
||||
let kind = match cast.kind {
|
||||
CastKind::PipeWire => "PipeWire",
|
||||
CastKind::WlrScreencopy => "wlr-screencopy",
|
||||
};
|
||||
println!(" Kind: {kind}");
|
||||
|
||||
match &cast.target {
|
||||
CastTarget::Nothing {} => {
|
||||
println!(" Target: nothing (cleared)");
|
||||
}
|
||||
CastTarget::Output { name } => {
|
||||
println!(" Target: output \"{name}\"");
|
||||
}
|
||||
CastTarget::Window { id } => {
|
||||
println!(" Target: window {id}");
|
||||
}
|
||||
}
|
||||
|
||||
if cast.is_dynamic_target {
|
||||
println!(" Dynamic cast target");
|
||||
}
|
||||
|
||||
if let Some(pid) = cast.pid {
|
||||
println!(" PID: {pid}");
|
||||
}
|
||||
|
||||
if let Some(node_id) = cast.pw_node_id {
|
||||
println!(" PipeWire node ID: {node_id}");
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_rounded(x: f64) -> String {
|
||||
let r = x.round();
|
||||
if (r - x).abs() <= 0.005 {
|
||||
|
||||
@@ -450,6 +450,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let is_open = state.overview.is_open;
|
||||
Response::OverviewState(Overview { is_open })
|
||||
}
|
||||
Request::Casts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let casts = state.casts.casts.values().cloned().collect();
|
||||
Response::Casts(casts)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
@@ -793,6 +798,121 @@ impl State {
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_casts(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_casts");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.casts;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
// Check PipeWire screencasts.
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
{
|
||||
// Check pending dynamic casts.
|
||||
for pending in &self.niri.casting.pending_dynamic_casts {
|
||||
let stream_id = pending.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
// Pending dynamic casts don't change any properties, so we only need to check if
|
||||
// it's missing from the state.
|
||||
if !state.casts.contains_key(&stream_id) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: pending.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::PipeWire,
|
||||
target: niri_ipc::CastTarget::Nothing {},
|
||||
is_dynamic_target: true,
|
||||
is_active: false,
|
||||
pid: None,
|
||||
pw_node_id: None,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
|
||||
// Check active casts.
|
||||
for cast in &self.niri.casting.casts {
|
||||
let stream_id = cast.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
let pw_node_id = cast.node_id();
|
||||
if state.casts.get(&stream_id).is_none_or(|existing| {
|
||||
// Only these properties can change.
|
||||
existing.is_active != cast.is_active()
|
||||
|| !cast.target.matches(&existing.target)
|
||||
|| existing.pw_node_id != pw_node_id
|
||||
}) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: cast.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::PipeWire,
|
||||
target: cast.target.make_ipc(),
|
||||
is_dynamic_target: cast.dynamic_target,
|
||||
is_active: cast.is_active(),
|
||||
pid: None,
|
||||
pw_node_id,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check screencopy casts.
|
||||
//
|
||||
// First, clear expired casts. Ideally we'd have a deadline timer, but our 1 second frame
|
||||
// callback timer calls refresh regularly, so that's fine as is.
|
||||
self.niri.screencopy_state.clear_expired_casts();
|
||||
|
||||
for queue in self.niri.screencopy_state.queues() {
|
||||
if let Some(cast_info) = queue.cast() {
|
||||
let stream_id = cast_info.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
if state.casts.get(&stream_id).is_none_or(|existing| {
|
||||
// Only this property can change.
|
||||
match &existing.target {
|
||||
niri_ipc::CastTarget::Output { name } => *name != cast_info.output_name,
|
||||
_ => true,
|
||||
}
|
||||
}) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: cast_info.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::WlrScreencopy,
|
||||
target: niri_ipc::CastTarget::Output {
|
||||
name: cast_info.output_name.clone(),
|
||||
},
|
||||
is_dynamic_target: false,
|
||||
is_active: true,
|
||||
pid: queue.credentials().map(|creds| creds.pid),
|
||||
pw_node_id: None,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stopped casts.
|
||||
for stream_id in state.casts.keys() {
|
||||
if !seen.contains(stream_id) {
|
||||
events.push(Event::CastStopped {
|
||||
stream_id: *stream_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_config_loaded(&mut self, failed: bool) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
|
||||
+45
-27
@@ -1,8 +1,6 @@
|
||||
use niri_config::utils::MergeWith as _;
|
||||
use niri_config::{Config, LayerRule};
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
@@ -15,7 +13,8 @@ use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
use crate::render_helpers::surface::push_elements_from_surface_tree;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -156,14 +155,13 @@ impl MappedLayer {
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
pub fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
let location = location + self.bob_offset();
|
||||
@@ -179,40 +177,60 @@ impl MappedLayer {
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
));
|
||||
}
|
||||
|
||||
rv.normal = render_elements_from_surface_tree(
|
||||
push_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.normal
|
||||
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||
self.shadow
|
||||
.render(renderer, location, &mut |elem| push(elem.into()));
|
||||
}
|
||||
|
||||
rv
|
||||
pub fn render_popups<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
let location = location + self.bob_offset();
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
|
||||
push_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-22
@@ -7,7 +7,7 @@ pub mod mapped;
|
||||
pub use mapped::MappedLayer;
|
||||
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this layer surface with.
|
||||
pub opacity: Option<f32>,
|
||||
@@ -29,30 +29,10 @@ pub struct ResolvedLayerRules {
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
on: false,
|
||||
offset: None,
|
||||
softness: None,
|
||||
spread: None,
|
||||
draw_behind_window: None,
|
||||
color: None,
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: false,
|
||||
baba_is_float: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||
|
||||
let mut resolved = ResolvedLayerRules::empty();
|
||||
let mut resolved = ResolvedLayerRules::default();
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m: &Match| {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -229,14 +230,14 @@ impl ClosingWindow {
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
Rc::new([
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
]),
|
||||
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
|
||||
+7
-11
@@ -1053,15 +1053,14 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> Vec<FloatingSpaceRenderElement<R>> {
|
||||
let mut rv = Vec::new();
|
||||
|
||||
push: &mut dyn FnMut(FloatingSpaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
// Draw the closing windows on top of the other windows.
|
||||
@@ -1069,7 +1068,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
// FIXME: I guess this should rather preserve the stacking order when the window is closed.
|
||||
for closing in self.closing_windows.iter().rev() {
|
||||
let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
|
||||
rv.push(elem.into());
|
||||
push(elem.into());
|
||||
}
|
||||
|
||||
let active = self.active_window_id.clone();
|
||||
@@ -1077,13 +1076,10 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
// For the active tile, draw the focus ring.
|
||||
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, focus_ring, target)
|
||||
.map(Into::into),
|
||||
);
|
||||
tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
@@ -220,18 +219,17 @@ impl FocusRing {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 8>::new();
|
||||
|
||||
push: &mut dyn FnMut(FocusRingRenderElement),
|
||||
) {
|
||||
if self.config.off {
|
||||
return rv.into_iter();
|
||||
return;
|
||||
}
|
||||
|
||||
let border_width = -self.locations[0].y;
|
||||
|
||||
// If drawing as a border with width = 0, then there's nothing to draw.
|
||||
if self.is_border && border_width == 0. {
|
||||
return rv.into_iter();
|
||||
return;
|
||||
}
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
@@ -244,7 +242,7 @@ impl FocusRing {
|
||||
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||
.into()
|
||||
};
|
||||
rv.push(elem);
|
||||
push(elem);
|
||||
};
|
||||
|
||||
if self.is_border {
|
||||
@@ -258,8 +256,6 @@ impl FocusRing {
|
||||
location + self.locations[0],
|
||||
);
|
||||
}
|
||||
|
||||
rv.into_iter()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 {
|
||||
|
||||
@@ -59,7 +59,8 @@ impl InsertHintElement {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
push: &mut dyn FnMut(FocusRingRenderElement),
|
||||
) {
|
||||
self.inner.render(renderer, location, push)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-37
@@ -64,7 +64,7 @@ use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::texture::TextureBuffer;
|
||||
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
|
||||
use crate::render_helpers::{BakedBuffer, RenderTarget};
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
use crate::utils::{
|
||||
@@ -159,7 +159,11 @@ pub trait LayoutElement {
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>>;
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
self.render_popups(renderer, location, scale, alpha, target, push);
|
||||
self.render_normal(renderer, location, scale, alpha, target, push);
|
||||
}
|
||||
|
||||
/// Renders the non-popup parts of the element.
|
||||
fn render_normal<R: NiriRenderer>(
|
||||
@@ -169,8 +173,9 @@ pub trait LayoutElement {
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
self.render(renderer, location, scale, alpha, target).normal
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let _ = (renderer, location, scale, alpha, target, push);
|
||||
}
|
||||
|
||||
/// Renders the popups of the element.
|
||||
@@ -181,8 +186,9 @@ pub trait LayoutElement {
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
self.render(renderer, location, scale, alpha, target).popups
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let _ = (renderer, location, scale, alpha, target, push);
|
||||
}
|
||||
|
||||
/// Requests the element to change its size.
|
||||
@@ -498,6 +504,7 @@ pub enum HitType {
|
||||
enum OverviewProgress {
|
||||
Animation(Animation),
|
||||
Gesture(OverviewGesture),
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -628,6 +635,7 @@ impl OverviewProgress {
|
||||
match self {
|
||||
OverviewProgress::Animation(anim) => anim.value(),
|
||||
OverviewProgress::Gesture(gesture) => gesture.value,
|
||||
OverviewProgress::Open => 1.,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2648,9 +2656,11 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.overview_open {
|
||||
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
|
||||
if anim.is_done() {
|
||||
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
|
||||
if anim.is_done() {
|
||||
if self.overview_open {
|
||||
self.overview_progress = Some(OverviewProgress::Open);
|
||||
} else {
|
||||
self.overview_progress = None;
|
||||
}
|
||||
}
|
||||
@@ -2674,19 +2684,19 @@ impl<W: LayoutElement> Layout<W> {
|
||||
pub fn are_animations_ongoing(&self, output: Option<&Output>) -> bool {
|
||||
// Keep advancing animations if we might need to scroll the view.
|
||||
if let Some(dnd) = &self.dnd {
|
||||
if output.map_or(true, |output| *output == dnd.output) {
|
||||
if output.is_none_or(|output| *output == dnd.output) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if output.map_or(true, |output| *output == move_.output) {
|
||||
if output.is_none_or(|output| *output == move_.output) {
|
||||
if move_.tile.are_animations_ongoing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep advancing animations if we might need to scroll the view.
|
||||
if !move_.is_floating {
|
||||
if !move_.is_floating || self.overview_open {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2720,7 +2730,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
|
||||
if output.map_or(true, |output| move_.output == *output) {
|
||||
if output.is_none_or(|output| move_.output == *output) {
|
||||
let pos_within_output = move_.tile_render_location(zoom);
|
||||
let view_rect =
|
||||
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
|
||||
@@ -2741,7 +2751,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
};
|
||||
|
||||
for (idx, mon) in monitors.iter_mut().enumerate() {
|
||||
if output.map_or(true, |output| mon.output == *output) {
|
||||
if output.is_none_or(|output| mon.output == *output) {
|
||||
let is_active = self.is_active
|
||||
&& idx == *active_monitor_idx
|
||||
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
|
||||
@@ -3271,7 +3281,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
let mon = &mut monitors[mon_idx];
|
||||
let activate = activate.map_smart(|| {
|
||||
window.map_or(true, |win| {
|
||||
window.is_none_or(|win| {
|
||||
mon_idx == *active_monitor_idx
|
||||
&& mon.active_window().map(|win| win.id()) == Some(win)
|
||||
})
|
||||
@@ -4709,38 +4719,37 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_interactive_move_for_output<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
pub fn render_interactive_move_for_output<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
output: &Output,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
|
||||
push: &mut dyn FnMut(RescaleRenderElement<TileRenderElement<R>>),
|
||||
) {
|
||||
if self.update_render_elements_time != self.clock.now() {
|
||||
error!("clock moved between updating render elements and rendering");
|
||||
}
|
||||
|
||||
let mut rv = None;
|
||||
let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if &move_.output == output {
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let location = move_.tile_render_location(zoom);
|
||||
let iter = move_
|
||||
.tile
|
||||
.render(renderer, location, true, target)
|
||||
.map(move |elem| {
|
||||
RescaleRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
)
|
||||
});
|
||||
rv = Some(iter);
|
||||
}
|
||||
if &move_.output != output {
|
||||
return;
|
||||
}
|
||||
|
||||
rv.into_iter().flatten()
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let location = move_.tile_render_location(zoom);
|
||||
move_
|
||||
.tile
|
||||
.render(renderer, location, true, target, &mut |elem| {
|
||||
push(RescaleRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, is_active: bool) {
|
||||
|
||||
+94
-110
@@ -282,6 +282,7 @@ impl From<&super::OverviewProgress> for OverviewProgress {
|
||||
match value {
|
||||
super::OverviewProgress::Animation(anim) => Self::Animation(anim.clone()),
|
||||
super::OverviewProgress::Gesture(gesture) => Self::Value(gesture.value),
|
||||
super::OverviewProgress::Open => Self::Value(1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -870,9 +871,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let new_id = self.workspaces[new_idx].id();
|
||||
|
||||
let activate = activate.map_smart(|| {
|
||||
window.map_or(true, |win| {
|
||||
self.active_window().map(|win| win.id()) == Some(win)
|
||||
})
|
||||
window.is_none_or(|win| self.active_window().map(|win| win.id()) == Some(win))
|
||||
});
|
||||
|
||||
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||
@@ -1491,6 +1490,13 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
(0..=self.workspaces.len()).map(move |idx| {
|
||||
let y = first_ws_y + idx as f64 * ws_height_with_gap;
|
||||
let loc = Point::from((0., y)) + static_offset;
|
||||
|
||||
// Even though all components that go into loc are rounded to physical pixels, the
|
||||
// floating point addition may lose precision. This can result for example in the
|
||||
// current workspace having y = 0.0000000000002 and thus missing pointer hits at the
|
||||
// monitor edge with y = 0. So, post-round the location too.
|
||||
let loc = loc.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
Rectangle::new(loc, ws_size)
|
||||
})
|
||||
}
|
||||
@@ -1639,40 +1645,36 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
pub fn render_insert_hint_between_workspaces<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> {
|
||||
let mut rv = None;
|
||||
|
||||
if !self.options.layout.insert_hint.off {
|
||||
if let Some(render_loc) = self.insert_hint_render_loc {
|
||||
if let InsertWorkspace::NewAt(_) = render_loc.workspace {
|
||||
let iter = self
|
||||
.insert_hint_element
|
||||
.render(renderer, render_loc.location)
|
||||
.map(MonitorInnerRenderElement::UncroppedInsertHint);
|
||||
rv = Some(iter);
|
||||
}
|
||||
}
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
if self.options.layout.insert_hint.off {
|
||||
return;
|
||||
}
|
||||
let Some(render_loc) = self.insert_hint_render_loc else {
|
||||
return;
|
||||
};
|
||||
let InsertWorkspace::NewAt(_) = render_loc.workspace else {
|
||||
return;
|
||||
};
|
||||
|
||||
rv.into_iter().flatten().map(|elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::default(), 1.);
|
||||
RelocateRenderElement::from_element(elem, Point::default(), Relocate::Relative)
|
||||
})
|
||||
self.insert_hint_element
|
||||
.render(renderer, render_loc.location, &mut |elem| {
|
||||
let elem = MonitorInnerRenderElement::UncroppedInsertHint(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::default(), 1.);
|
||||
let elem =
|
||||
RelocateRenderElement::from_element(elem, Point::default(), Relocate::Relative);
|
||||
push(elem);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_elements<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
pub fn render_workspaces<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
Rectangle<f64, Logical>,
|
||||
MonitorRenderElement<R>,
|
||||
impl Iterator<Item = MonitorRenderElement<R>> + 'a,
|
||||
),
|
||||
> {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Monitor::render_workspaces");
|
||||
|
||||
let scale = self.scale.fractional_scale();
|
||||
// Ceil the height in physical pixels.
|
||||
@@ -1702,95 +1704,77 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
|
||||
// Draw the insert hint.
|
||||
let mut insert_hint = None;
|
||||
if !self.options.layout.insert_hint.off {
|
||||
if let Some(render_loc) = self.insert_hint_render_loc {
|
||||
if let InsertWorkspace::Existing(workspace_id) = render_loc.workspace {
|
||||
insert_hint = Some((
|
||||
workspace_id,
|
||||
self.insert_hint_element
|
||||
.render(renderer, render_loc.location),
|
||||
));
|
||||
let insert_hint_render_loc = self
|
||||
.insert_hint_render_loc
|
||||
.filter(|_| !self.options.layout.insert_hint.off);
|
||||
|
||||
let scale_relocate = move |geo: Rectangle<f64, Logical>, elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_geo() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
};
|
||||
|
||||
for (ws, geo) in self.workspaces_with_render_geo() {
|
||||
// Macro instead of closure because ws and insert hint have different elem types.
|
||||
macro_rules! push {
|
||||
() => {{
|
||||
&mut |elem| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds);
|
||||
if let Some(elem) = elem {
|
||||
let elem = MonitorInnerRenderElement::from(elem);
|
||||
push(scale_relocate(geo, elem));
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
ws.render_floating(renderer, target, focus_ring, push!());
|
||||
|
||||
if let Some(loc) = insert_hint_render_loc {
|
||||
if loc.workspace == InsertWorkspace::Existing(ws.id()) {
|
||||
self.insert_hint_element
|
||||
.render(renderer, loc.location, push!());
|
||||
}
|
||||
}
|
||||
|
||||
ws.render_scrolling(renderer, target, focus_ring, push!());
|
||||
}
|
||||
|
||||
self.workspaces_with_render_geo().map(move |(ws, geo)| {
|
||||
let map_ws_contents = move |elem: WorkspaceRenderElement<R>| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?;
|
||||
let elem = MonitorInnerRenderElement::Workspace(elem);
|
||||
Some(elem)
|
||||
};
|
||||
|
||||
let (floating, scrolling) = ws.render_elements(renderer, target, focus_ring);
|
||||
let floating = floating.filter_map(map_ws_contents);
|
||||
let scrolling = scrolling.filter_map(map_ws_contents);
|
||||
|
||||
let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) {
|
||||
let iter = insert_hint.take().unwrap().1;
|
||||
let iter = iter.filter_map(move |elem| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?;
|
||||
let elem = MonitorInnerRenderElement::InsertHint(elem);
|
||||
Some(elem)
|
||||
});
|
||||
Some(iter)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hint = hint.into_iter().flatten();
|
||||
|
||||
let iter = floating.chain(hint).chain(scrolling);
|
||||
|
||||
let scale_relocate = move |elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_positions() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
};
|
||||
|
||||
let iter = iter.map(scale_relocate);
|
||||
|
||||
let background = ws.render_background();
|
||||
let background = scale_relocate(MonitorInnerRenderElement::SolidColor(background));
|
||||
|
||||
(geo, background, iter)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_workspace_shadows<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a {
|
||||
pub fn render_workspace_shadows<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
let Some(progress) = self.overview_progress.as_ref().map(|p| p.clamped_value()) else {
|
||||
return;
|
||||
};
|
||||
let alpha = progress.clamp(0., 1.) as f32;
|
||||
|
||||
let _span = tracy_client::span!("Monitor::render_workspace_shadows");
|
||||
|
||||
let scale = self.scale.fractional_scale();
|
||||
let zoom = self.overview_zoom();
|
||||
let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
|
||||
|
||||
self.workspaces_with_render_geo()
|
||||
.flat_map(move |(ws, geo)| {
|
||||
let shadow = overview_clamped_progress.map(|value| {
|
||||
ws.render_shadow(renderer)
|
||||
.map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
|
||||
.map(MonitorInnerRenderElement::Shadow)
|
||||
});
|
||||
let iter = shadow.into_iter().flatten();
|
||||
|
||||
iter.map(move |elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
})
|
||||
for (ws, geo) in self.workspaces_with_render_geo() {
|
||||
ws.render_shadow(renderer, &mut |elem| {
|
||||
let elem = elem.with_alpha(alpha);
|
||||
let elem = MonitorInnerRenderElement::Shadow(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
push(elem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -39,8 +40,6 @@ impl OpenAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.anim.is_done()
|
||||
}
|
||||
@@ -104,14 +103,14 @@ impl OpenAnimation {
|
||||
None,
|
||||
scale.x as f32,
|
||||
alpha,
|
||||
vec![
|
||||
Rc::new([
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
]),
|
||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
|
||||
+52
-37
@@ -336,8 +336,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for tile in self.tiles_mut() {
|
||||
tile.update_shaders();
|
||||
for col in &mut self.columns {
|
||||
col.update_shaders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,6 +986,23 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.data.insert(idx, ColumnData::new(&column));
|
||||
self.columns.insert(idx, column);
|
||||
|
||||
if !was_empty && idx <= self.active_column_idx {
|
||||
self.active_column_idx += 1;
|
||||
}
|
||||
|
||||
// Animate movement of other columns.
|
||||
let offset = self.column_x(idx + 1) - self.column_x(idx);
|
||||
let config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
||||
if self.active_column_idx <= idx {
|
||||
for col in &mut self.columns[idx + 1..] {
|
||||
col.animate_move_from_with_config(-offset, config);
|
||||
}
|
||||
} else {
|
||||
for col in &mut self.columns[..idx] {
|
||||
col.animate_move_from_with_config(offset, config);
|
||||
}
|
||||
}
|
||||
|
||||
if activate {
|
||||
// If this is the first window on an empty workspace, remove the effect of whatever
|
||||
// view_offset was left over and skip the animation.
|
||||
@@ -1002,21 +1019,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0);
|
||||
self.activate_column_with_anim_config(idx, anim_config);
|
||||
self.activate_prev_column_on_removal = prev_offset;
|
||||
} else if !was_empty && idx <= self.active_column_idx {
|
||||
self.active_column_idx += 1;
|
||||
}
|
||||
|
||||
// Animate movement of other columns.
|
||||
let offset = self.column_x(idx + 1) - self.column_x(idx);
|
||||
let config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
||||
if self.active_column_idx <= idx {
|
||||
for col in &mut self.columns[idx + 1..] {
|
||||
col.animate_move_from_with_config(-offset, config);
|
||||
}
|
||||
} else {
|
||||
for col in &mut self.columns[..idx] {
|
||||
col.animate_move_from_with_config(offset, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,11 +1386,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
// We might need to move the view to ensure the resized window is still visible. But
|
||||
// only do it when the view isn't frozen by an interactive resize or a view gesture.
|
||||
if self.interactive_resize.is_none() && !self.view_offset.is_gesture() {
|
||||
// Restore the view offset upon unfullscreening if needed.
|
||||
if let Some(prev_offset) = unfullscreen_offset {
|
||||
self.animate_view_offset(col_idx, prev_offset);
|
||||
}
|
||||
|
||||
// Synchronize the horizontal view movement with the resize so that it looks nice.
|
||||
// This is especially important for always-centered view.
|
||||
let config = if ongoing_resize_anim {
|
||||
@@ -1397,6 +1394,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.options.animations.horizontal_view_movement.0
|
||||
};
|
||||
|
||||
// Restore the view offset upon unfullscreening if needed.
|
||||
if let Some(prev_offset) = unfullscreen_offset {
|
||||
self.animate_view_offset_with_config(col_idx, prev_offset, config);
|
||||
}
|
||||
|
||||
// FIXME: we will want to skip the animation in some cases here to make continuously
|
||||
// resizing windows not look janky.
|
||||
self.animate_view_offset_to_column_with_config(None, col_idx, None, config);
|
||||
@@ -1856,7 +1858,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.activate_prev_column_on_removal = None;
|
||||
}
|
||||
|
||||
if target_column_idx < self.active_column_idx {
|
||||
if target_column_idx <= self.active_column_idx {
|
||||
// Tiles to the left animate from the following column.
|
||||
offset.x += self.column_x(target_column_idx + 1) - self.column_x(target_column_idx);
|
||||
}
|
||||
@@ -2895,25 +2897,24 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
.is_fullscreen()
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> Vec<ScrollingSpaceRenderElement<R>> {
|
||||
let mut rv = vec![];
|
||||
|
||||
push: &mut dyn FnMut(ScrollingSpaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
// Draw the closing windows on top of the other windows.
|
||||
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
|
||||
for closing in self.closing_windows.iter().rev() {
|
||||
let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
|
||||
rv.push(elem.into());
|
||||
push(elem.into());
|
||||
}
|
||||
|
||||
if self.columns.is_empty() {
|
||||
return rv;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut first = true;
|
||||
@@ -2928,7 +2929,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
{
|
||||
let pos = view_off + col_off + col_render_off;
|
||||
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into));
|
||||
col.tab_indicator
|
||||
.render(renderer, pos, &mut |elem| push(elem.into()));
|
||||
}
|
||||
|
||||
for (tile, tile_off, visible) in col.tiles_in_render_order() {
|
||||
@@ -2953,14 +2955,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
continue;
|
||||
}
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, focus_ring, target)
|
||||
.map(Into::into),
|
||||
);
|
||||
tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
||||
@@ -3493,7 +3492,15 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
if gesture.dnd_last_event_time.is_some() && gesture.tracker.pos() == 0. {
|
||||
// DnD didn't scroll anything, so preserve the current view position (rather than
|
||||
// snapping the window).
|
||||
self.view_offset = ViewOffset::Static(gesture.delta_from_tracker);
|
||||
|
||||
// If there's an ongoing animation within the gesture (e.g. from a window being removed
|
||||
// during DnD), preserve it.
|
||||
if let Some(mut anim) = gesture.animation.take() {
|
||||
anim.offset(gesture.current_view_offset);
|
||||
self.view_offset = ViewOffset::Animation(anim);
|
||||
} else {
|
||||
self.view_offset = ViewOffset::Static(gesture.delta_from_tracker);
|
||||
}
|
||||
|
||||
if !self.columns.is_empty() {
|
||||
// Just in case, make sure the active window remains on screen.
|
||||
@@ -4054,6 +4061,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for tile in &mut self.tiles {
|
||||
tile.update_shaders();
|
||||
}
|
||||
|
||||
self.tab_indicator.update_shaders();
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
if let Some(move_) = &mut self.move_animation {
|
||||
if move_.anim.is_done() {
|
||||
|
||||
@@ -166,19 +166,19 @@ impl Shadow {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
push: &mut dyn FnMut(ShadowRenderElement),
|
||||
) {
|
||||
if !self.config.on {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
|
||||
if !has_shadow_shader {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_rects)
|
||||
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
for (shader, rect) in zip(&self.shaders, &self.shader_rects) {
|
||||
push(shader.clone().with_location(location + rect.loc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,17 +294,17 @@ impl TabIndicator {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
|
||||
push: &mut dyn FnMut(TabIndicatorRenderElement),
|
||||
) {
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if !has_border_shader {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_locs)
|
||||
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
|
||||
.map(TabIndicatorRenderElement::from);
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
for (shader, loc) in zip(&self.shaders, &self.shader_locs) {
|
||||
let elem = shader.clone().with_location(pos + *loc);
|
||||
push(TabIndicatorRenderElement::from(elem));
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra size occupied by the tab indicator.
|
||||
|
||||
+63
-11
@@ -166,17 +166,6 @@ impl LayoutElement for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
_alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
SplitElements::default()
|
||||
}
|
||||
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
@@ -3674,6 +3663,69 @@ fn tabs_with_different_border() {
|
||||
check_ops_with_options(options, ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expel_pending_left_from_fullscreen_tabbed_column() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::FullscreenWindow(1),
|
||||
Op::Communicate(1),
|
||||
// 1 is now fullscreen, view_offset_to_restore is set.
|
||||
Op::ToggleColumnTabbedDisplay,
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(2),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft { id: Some(2) },
|
||||
// 2 is consumed into a fullscreen column, fullscreen is requested but not applied.
|
||||
//
|
||||
// Now, get it back out while keeping it focused.
|
||||
//
|
||||
// Importantly, we expel it *left*, which results in adding a new column with the exact
|
||||
// same active_column_idx.
|
||||
Op::FocusWindow(2),
|
||||
Op::ConsumeOrExpelWindowLeft { id: None },
|
||||
];
|
||||
|
||||
check_ops(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_render_geo_at_fractional_scale() {
|
||||
let ops = [
|
||||
Op::AddScaledOutput {
|
||||
id: 1,
|
||||
scale: 1.1,
|
||||
layout_config: None,
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::FocusWorkspaceDown,
|
||||
Op::CompleteAnimations,
|
||||
];
|
||||
|
||||
let layout = check_ops(ops);
|
||||
|
||||
let MonitorSet::Normal { monitors, .. } = &layout.monitor_set else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mon = &monitors[0];
|
||||
let mut iter = mon.workspaces_with_render_geo();
|
||||
let (_ws, geo) = iter.next().unwrap();
|
||||
assert!(
|
||||
iter.next().is_none(),
|
||||
"animations are completed, only one workspace should be visible"
|
||||
);
|
||||
assert_eq!(
|
||||
geo.loc.y, 0.,
|
||||
"active workspace must be at y = 0 exactly, \
|
||||
otherwise a pointer against the screen edge at y = 0 won't hit it"
|
||||
);
|
||||
}
|
||||
|
||||
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
|
||||
if parent_id == id {
|
||||
return true;
|
||||
|
||||
@@ -338,6 +338,65 @@ fn interactive_move_unfullscreen_to_floating_stops_dnd_scroll() {
|
||||
check_ops(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_move_restore_to_floating_animates_view_offset() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(2),
|
||||
},
|
||||
// Toggle window 1 to floating.
|
||||
Op::FocusWindow(1),
|
||||
Op::ToggleWindowFloating { id: None },
|
||||
// Fullscreen window 1 - it moves to scrolling with restore_to_floating = true.
|
||||
Op::FullscreenWindow(1),
|
||||
Op::Communicate(1),
|
||||
Op::CompleteAnimations,
|
||||
];
|
||||
|
||||
let mut layout = check_ops(ops);
|
||||
|
||||
// Verify window 1 is in scrolling and has restore_to_floating = true.
|
||||
let scrolling = layout.active_workspace().unwrap().scrolling();
|
||||
let tile1 = scrolling.tiles().find(|t| *t.window().id() == 1).unwrap();
|
||||
assert!(
|
||||
tile1.restore_to_floating,
|
||||
"window 1 should have restore_to_floating = true"
|
||||
);
|
||||
|
||||
let ops = [
|
||||
// Start interactive move on window 1.
|
||||
Op::InteractiveMoveBegin {
|
||||
window: 1,
|
||||
output_idx: 1,
|
||||
px: 100.,
|
||||
py: 100.,
|
||||
},
|
||||
// Update with a large delta to trigger the unmaximize.
|
||||
Op::InteractiveMoveUpdate {
|
||||
window: 1,
|
||||
dx: 1000.,
|
||||
dy: 1000.,
|
||||
output_idx: 1,
|
||||
px: 0.,
|
||||
py: 0.,
|
||||
},
|
||||
];
|
||||
check_ops_on_layout(&mut layout, ops);
|
||||
|
||||
// Window 1 should now be removed from the workspace (in the interactive move state).
|
||||
// Window 2 should be the only window in the scrolling space.
|
||||
let scrolling = layout.active_workspace().unwrap().scrolling();
|
||||
assert_eq!(scrolling.tiles().count(), 1);
|
||||
assert!(scrolling.tiles().next().unwrap().window().id() == &2);
|
||||
|
||||
// The view offset should be animating to show window 2.
|
||||
assert!(scrolling.view_offset().is_animation_ongoing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfullscreen_view_offset_not_reset_during_dnd_gesture() {
|
||||
let ops = [
|
||||
|
||||
+110
-97
@@ -1007,13 +1007,14 @@ impl<W: LayoutElement> Tile<W> {
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
fn render_inner<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render_inner");
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
@@ -1041,7 +1042,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let location = location + self.bob_offset();
|
||||
|
||||
let window_loc = self.window_loc();
|
||||
let window_size = self.window_size().to_f64();
|
||||
let window_size = self.window_size();
|
||||
let animated_window_size = self.animated_window_size();
|
||||
let window_render_loc = location + window_loc;
|
||||
let area = Rectangle::new(window_render_loc, animated_window_size);
|
||||
@@ -1056,29 +1057,31 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.unwrap_or_default()
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
|
||||
// Popups go on top, whether it's resize or not.
|
||||
self.window.render_popups(
|
||||
renderer,
|
||||
window_render_loc,
|
||||
scale,
|
||||
win_alpha,
|
||||
target,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
|
||||
// If we're resizing, try to render a shader, or a fallback.
|
||||
let mut resize_shader = None;
|
||||
let mut resize_popups = None;
|
||||
let mut resize_fallback = None;
|
||||
|
||||
let mut pushed_resize = false;
|
||||
if let Some(resize) = &self.resize_animation {
|
||||
resize_popups = Some(
|
||||
self.window
|
||||
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
);
|
||||
|
||||
if ResizeRenderElement::has_shader(renderer) {
|
||||
let gles_renderer = renderer.as_gles_renderer();
|
||||
|
||||
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
|
||||
let window_elements = self.window.render_normal(
|
||||
let mut window_elements = Vec::new();
|
||||
self.window.render_normal(
|
||||
gles_renderer,
|
||||
Point::from((0., 0.)),
|
||||
scale,
|
||||
1.,
|
||||
target,
|
||||
&mut |elem| window_elements.push(elem),
|
||||
);
|
||||
|
||||
let current = resize
|
||||
@@ -1125,46 +1128,33 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// This is not a problem for split popups as the code will look for them by
|
||||
// original id when it doesn't find them on the offscreen.
|
||||
self.window.set_offscreen_data(Some(data));
|
||||
resize_shader = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed_resize = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resize_shader.is_none() {
|
||||
if !pushed_resize {
|
||||
let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
|
||||
resize_fallback = Some(
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
area.loc,
|
||||
win_alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
area.loc,
|
||||
win_alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
push(elem.into());
|
||||
pushed_resize = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not resizing, render the window itself.
|
||||
let mut window_surface = None;
|
||||
let mut window_popups = None;
|
||||
let mut rounded_corner_damage = None;
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if resize_shader.is_none() && resize_fallback.is_none() {
|
||||
let window = self
|
||||
.window
|
||||
.render(renderer, window_render_loc, scale, win_alpha, target);
|
||||
|
||||
if !pushed_resize {
|
||||
let geo = Rectangle::new(window_render_loc, window_size);
|
||||
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
|
||||
|
||||
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
|
||||
|
||||
if clip_to_geometry && clip_shader.is_some() {
|
||||
let damage = self.rounded_corner_damage.element();
|
||||
rounded_corner_damage = Some(damage.with_location(window_render_loc).into());
|
||||
}
|
||||
|
||||
window_surface = Some(window.normal.into_iter().map(move |elem| match elem {
|
||||
let clip = |elem| match elem {
|
||||
LayoutElementRenderElement::Wayland(elem) => {
|
||||
// If we should clip to geometry, render a clipped window.
|
||||
if clip_to_geometry {
|
||||
@@ -1213,21 +1203,24 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// Otherwise, render the solid color as is.
|
||||
LayoutElementRenderElement::SolidColor(elem).into()
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
window_popups = Some(window.popups.into_iter().map(Into::into));
|
||||
if clip_to_geometry && clip_shader.is_some() {
|
||||
let damage = self.rounded_corner_damage.element();
|
||||
push(damage.with_location(window_render_loc).into());
|
||||
}
|
||||
|
||||
self.window.render_normal(
|
||||
renderer,
|
||||
window_render_loc,
|
||||
scale,
|
||||
win_alpha,
|
||||
target,
|
||||
&mut |elem| push(clip(elem)),
|
||||
);
|
||||
}
|
||||
|
||||
let rv = resize_popups
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(resize_shader)
|
||||
.chain(resize_fallback)
|
||||
.chain(window_popups.into_iter().flatten())
|
||||
.chain(rounded_corner_damage)
|
||||
.chain(window_surface.into_iter().flatten());
|
||||
|
||||
let elem = (fullscreen_progress > 0.).then(|| {
|
||||
if fullscreen_progress > 0. {
|
||||
let alpha = fullscreen_progress as f32;
|
||||
|
||||
// During the un/fullscreen animation, render a border element in order to use the
|
||||
@@ -1243,7 +1236,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
|
||||
let size = self.fullscreen_backdrop.size();
|
||||
let color = self.fullscreen_backdrop.color();
|
||||
BorderRenderElement::new(
|
||||
let elem = BorderRenderElement::new(
|
||||
size,
|
||||
Rectangle::from_size(size),
|
||||
GradientInterpolation::default(),
|
||||
@@ -1256,47 +1249,50 @@ impl<W: LayoutElement> Tile<W> {
|
||||
scale.x as f32,
|
||||
alpha,
|
||||
)
|
||||
.with_location(location)
|
||||
.into()
|
||||
.with_location(location);
|
||||
push(elem.into());
|
||||
} else {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&self.fullscreen_backdrop,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
);
|
||||
push(elem.into());
|
||||
}
|
||||
});
|
||||
let rv = rv.chain(elem);
|
||||
}
|
||||
|
||||
let elem = self.visual_border_width().map(|width| {
|
||||
self.border
|
||||
.render(renderer, location + Point::from((width, width)))
|
||||
.map(Into::into)
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
if let Some(width) = self.visual_border_width() {
|
||||
self.border.render(
|
||||
renderer,
|
||||
location + Point::from((width, width)),
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
// Hide the focus ring when maximized/fullscreened. It's not normally visible anyway due to
|
||||
// being outside the monitor or obscured by a solid colored bar, but it is visible under
|
||||
// semitransparent bars in maximized state (which is a bit weird) and in the overview (also
|
||||
// a bit weird).
|
||||
let elem = (focus_ring && expanded_progress < 1.)
|
||||
.then(|| self.focus_ring.render(renderer, location).map(Into::into));
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
if focus_ring && expanded_progress < 1. {
|
||||
self.focus_ring
|
||||
.render(renderer, location, &mut |elem| push(elem.into()));
|
||||
}
|
||||
|
||||
let elem = (expanded_progress < 1.)
|
||||
.then(|| self.shadow.render(renderer, location).map(Into::into));
|
||||
rv.chain(elem.into_iter().flatten())
|
||||
if expanded_progress < 1. {
|
||||
self.shadow
|
||||
.render(renderer, location, &mut |elem| push(elem.into()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render");
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
@@ -1306,16 +1302,19 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.as_ref()
|
||||
.map_or(1., |alpha| alpha.anim.clamped_value()) as f32;
|
||||
|
||||
let mut open_anim_elem = None;
|
||||
let mut alpha_anim_elem = None;
|
||||
let mut window_elems = None;
|
||||
|
||||
let mut pushed = false;
|
||||
self.window().set_offscreen_data(None);
|
||||
|
||||
if let Some(open) = &self.open_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
let mut elements = Vec::new();
|
||||
self.render_inner(
|
||||
renderer,
|
||||
Point::from((0., 0.)),
|
||||
focus_ring,
|
||||
target,
|
||||
&mut |elem| elements.push(elem),
|
||||
);
|
||||
match open.render(
|
||||
renderer,
|
||||
&elements,
|
||||
@@ -1326,7 +1325,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
) {
|
||||
Ok((elem, data)) => {
|
||||
self.window().set_offscreen_data(Some(data));
|
||||
open_anim_elem = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering window opening animation: {err:?}");
|
||||
@@ -1334,15 +1334,22 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
} else if let Some(alpha) = &self.alpha_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
let mut elements = Vec::new();
|
||||
self.render_inner(
|
||||
renderer,
|
||||
Point::from((0., 0.)),
|
||||
focus_ring,
|
||||
target,
|
||||
&mut |elem| elements.push(elem),
|
||||
);
|
||||
match alpha.offscreen.render(renderer, scale, &elements) {
|
||||
Ok((elem, _sync, data)) => {
|
||||
let offset = elem.offset();
|
||||
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
|
||||
|
||||
self.window().set_offscreen_data(Some(data));
|
||||
alpha_anim_elem = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
|
||||
@@ -1350,14 +1357,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
|
||||
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
|
||||
if !pushed {
|
||||
self.render_inner(renderer, location, focus_ring, target, &mut |elem| {
|
||||
push(elem)
|
||||
});
|
||||
}
|
||||
|
||||
open_anim_elem
|
||||
.into_iter()
|
||||
.chain(alpha_anim_elem)
|
||||
.chain(window_elems.into_iter().flatten())
|
||||
}
|
||||
|
||||
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
|
||||
@@ -1371,19 +1375,28 @@ impl<W: LayoutElement> Tile<W> {
|
||||
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
|
||||
let _span = tracy_client::span!("Tile::render_snapshot");
|
||||
|
||||
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
|
||||
let mut contents = Vec::new();
|
||||
self.render(
|
||||
renderer,
|
||||
Point::from((0., 0.)),
|
||||
false,
|
||||
RenderTarget::Output,
|
||||
&mut |elem| contents.push(elem),
|
||||
);
|
||||
|
||||
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
|
||||
let blocked_out_contents = self.render(
|
||||
let mut blocked_out_contents = Vec::new();
|
||||
self.render(
|
||||
renderer,
|
||||
Point::from((0., 0.)),
|
||||
false,
|
||||
RenderTarget::Screencast,
|
||||
&mut |elem| blocked_out_contents.push(elem),
|
||||
);
|
||||
|
||||
RenderSnapshot {
|
||||
contents: contents.collect(),
|
||||
blocked_out_contents: blocked_out_contents.collect(),
|
||||
contents,
|
||||
blocked_out_contents,
|
||||
block_out_from: self.window.rules().block_out_from,
|
||||
size: self.animated_tile_size(),
|
||||
texture: Default::default(),
|
||||
|
||||
+30
-21
@@ -1386,7 +1386,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
pub fn toggle_window_floating(&mut self, id: Option<&W::Id>) {
|
||||
let active_id = self.active_window().map(|win| win.id().clone());
|
||||
let target_is_active = id.map_or(true, |id| Some(id) == active_id.as_ref());
|
||||
let target_is_active = id.is_none_or(|id| Some(id) == active_id.as_ref());
|
||||
let Some(id) = id.cloned().or(active_id) else {
|
||||
return;
|
||||
};
|
||||
@@ -1624,39 +1624,48 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
pub fn render_scrolling<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> (
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
|
||||
let scrolling = self
|
||||
.scrolling
|
||||
.render_elements(renderer, target, scrolling_focus_ring);
|
||||
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
|
||||
self.scrolling
|
||||
.render(renderer, target, scrolling_focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_floating<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
if !self.is_floating_visible() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
let floating_focus_ring = focus_ring && self.floating_is_active();
|
||||
let floating = self.is_floating_visible().then(|| {
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
let floating =
|
||||
self.floating
|
||||
.render_elements(renderer, view_rect, target, floating_focus_ring);
|
||||
floating.into_iter().map(WorkspaceRenderElement::from)
|
||||
});
|
||||
let floating = floating.into_iter().flatten();
|
||||
|
||||
(floating, scrolling)
|
||||
self.floating.render(
|
||||
renderer,
|
||||
view_rect,
|
||||
target,
|
||||
floating_focus_ring,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)))
|
||||
push: &mut dyn FnMut(ShadowRenderElement),
|
||||
) {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)), push);
|
||||
}
|
||||
|
||||
pub fn render_background(&self) -> SolidColorRenderElement {
|
||||
|
||||
+2
-8
@@ -19,17 +19,11 @@ pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod rubber_band;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod screencasting;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+2
-2
@@ -7,6 +7,7 @@ use std::io::{self, Write};
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{env, mem};
|
||||
|
||||
use calloop::EventLoop;
|
||||
@@ -26,7 +27,6 @@ use niri::utils::spawning::{
|
||||
use niri::utils::{cause_panic, version, watcher, xwayland, IS_SYSTEMD_SERVICE};
|
||||
use niri_config::{Config, ConfigPath};
|
||||
use niri_ipc::socket::SOCKET_PATH_ENV;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
@@ -228,7 +228,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
state.niri.a11y.start();
|
||||
}
|
||||
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
|
||||
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]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
|
||||
+361
-782
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ pub mod gamma_control;
|
||||
pub mod mutter_x11_interop;
|
||||
pub mod output_management;
|
||||
pub mod screencopy;
|
||||
pub mod virtual_keyboard;
|
||||
pub mod virtual_pointer;
|
||||
|
||||
pub mod raw;
|
||||
|
||||
+261
-27
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -9,11 +9,7 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
Flags, ZwlrScreencopyFrameV1,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::output::{Output, WeakOutput};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
|
||||
};
|
||||
@@ -24,49 +20,181 @@ use smithay::reexports::wayland_server::{
|
||||
};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
||||
use smithay::wayland::{dmabuf, shm};
|
||||
use wayland_backend::server::Credentials;
|
||||
use zwlr_screencopy_frame_v1::{Flags, ZwlrScreencopyFrameV1};
|
||||
use zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::utils::{get_credentials_for_client, get_monotonic_time, CastSessionId, CastStreamId};
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
/// Inactivity timeout for considering a screencopy cast as stopped.
|
||||
///
|
||||
/// xdg-desktop-portal-wlr keeps the screencopy manager alive across casts, so there's no way to
|
||||
/// tell that a screencast had stopped. So we use a timeout: if no new with_damage frames are
|
||||
/// requested for this timeout, consider the screencast finished.
|
||||
const CAST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct ScreencopyQueue {
|
||||
/// Credentials of this wlr-screencopy client, if known.
|
||||
credentials: Option<Credentials>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
/// Frames waiting for the client to call copy or destroy.
|
||||
pending_frames: HashSet<ZwlrScreencopyFrameV1>,
|
||||
/// Queue of screencopies waiting for a corresponding output redraw with damage.
|
||||
screencopies: Vec<Screencopy>,
|
||||
/// Cast tracking, set when the first with_damage request arrives.
|
||||
cast: Option<ScreencopyCast>,
|
||||
}
|
||||
|
||||
impl Default for ScreencopyQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pub struct ScreencopyCast {
|
||||
pub session_id: CastSessionId,
|
||||
pub stream_id: CastStreamId,
|
||||
/// Output being captured.
|
||||
///
|
||||
/// Generally equal to the front entry in the queue, and persisted here when the queue becomes
|
||||
/// empty.
|
||||
pub output: WeakOutput,
|
||||
/// Cached name of the output.
|
||||
pub output_name: String,
|
||||
/// Deadline after which this cast is considered stopped if no new frames arrive.
|
||||
pub deadline: Duration,
|
||||
}
|
||||
|
||||
impl ScreencopyCast {
|
||||
fn new(output: &Output) -> Self {
|
||||
Self {
|
||||
session_id: CastSessionId::next(),
|
||||
stream_id: CastStreamId::next(),
|
||||
output: output.downgrade(),
|
||||
output_name: output.name(),
|
||||
deadline: get_monotonic_time() + CAST_TIMEOUT,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_deadline(&mut self) {
|
||||
self.deadline = get_monotonic_time() + CAST_TIMEOUT;
|
||||
}
|
||||
|
||||
fn update_output(&mut self, output: &Output) {
|
||||
// Only allocate a new name when the output differs.
|
||||
let weak = output.downgrade();
|
||||
if self.output != weak {
|
||||
self.output = weak;
|
||||
self.output_name = output.name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreencopyQueue {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(credentials: Option<Credentials>) -> Self {
|
||||
Self {
|
||||
damage_tracker: OutputDamageTracker::new((0, 0), 1.0, Transform::Normal),
|
||||
pending_frames: HashSet::new(),
|
||||
screencopies: Vec::new(),
|
||||
cast: None,
|
||||
credentials,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pending_frames.is_empty() && self.screencopies.is_empty()
|
||||
}
|
||||
|
||||
/// Get the cast tracking info, if this queue is tracking a cast.
|
||||
pub fn cast(&self) -> Option<&ScreencopyCast> {
|
||||
self.cast.as_ref()
|
||||
}
|
||||
|
||||
pub fn credentials(&self) -> Option<Credentials> {
|
||||
self.credentials
|
||||
}
|
||||
|
||||
pub fn split(&mut self) -> (&mut OutputDamageTracker, Option<&Screencopy>) {
|
||||
let ScreencopyQueue {
|
||||
damage_tracker,
|
||||
screencopies,
|
||||
..
|
||||
} = self;
|
||||
(damage_tracker, screencopies.first())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, screencopy: Screencopy) {
|
||||
// Screencopy without damage is rendered immediately without the queue.
|
||||
if !screencopy.with_damage() {
|
||||
error!("only screencopy with damage can be pushed in the queue");
|
||||
}
|
||||
|
||||
if let Some(cast) = &mut self.cast {
|
||||
// Update cast output when pushing a new front screencopy.
|
||||
if self.screencopies.is_empty() {
|
||||
cast.update_output(screencopy.output());
|
||||
}
|
||||
} else {
|
||||
// First with_damage request, mark this as a screencast.
|
||||
let output = screencopy.output();
|
||||
self.cast = Some(ScreencopyCast::new(output));
|
||||
}
|
||||
|
||||
self.screencopies.push(screencopy);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Screencopy {
|
||||
self.screencopies.pop().unwrap()
|
||||
let rv = self.screencopies.remove(0);
|
||||
|
||||
let cast = self.cast.as_mut().unwrap();
|
||||
if let Some(first) = self.screencopies.first() {
|
||||
// Update cast output (most of the time we expect this to be the same).
|
||||
cast.update_output(first.output());
|
||||
} else {
|
||||
// Queue became empty, update deadline for considering the cast stopped.
|
||||
cast.update_deadline();
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, output: &Output) {
|
||||
pub fn clear_expired_cast(&mut self) {
|
||||
if let Some(cast) = &self.cast {
|
||||
// Check deadline if there are no in-flight frames.
|
||||
if self.screencopies.is_empty() && cast.deadline <= get_monotonic_time() {
|
||||
self.cast = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_output(&mut self, output: &Output) {
|
||||
if self.screencopies.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.screencopies
|
||||
.retain(|screencopy| screencopy.output() != output);
|
||||
|
||||
if let Some(cast) = &mut self.cast {
|
||||
if self.screencopies.is_empty() {
|
||||
// Queue became empty, update deadline for considering the cast stopped.
|
||||
cast.update_deadline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_frame(&mut self, frame: &ZwlrScreencopyFrameV1) {
|
||||
self.pending_frames.remove(frame);
|
||||
|
||||
if self.screencopies.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.screencopies
|
||||
.retain(|screencopy| screencopy.frame != *frame);
|
||||
|
||||
if let Some(cast) = &mut self.cast {
|
||||
if self.screencopies.is_empty() {
|
||||
// Queue became empty, update deadline for considering the cast stopped.
|
||||
cast.update_deadline();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,23 +227,54 @@ impl ScreencopyManagerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, manager: &ZwlrScreencopyManagerV1) {
|
||||
// Clean up all entries if its manager is dead and its queue is empty.
|
||||
self.queues
|
||||
.retain(|k, v| k.is_alive() || !v.screencopies.is_empty());
|
||||
pub fn push(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
let Some(queue) = self.queues.get_mut(manager) else {
|
||||
// Destroying the manager does not invalidate existing frames, so the queue should
|
||||
// keep existing.
|
||||
error!("screencopy queue must not be deleted as long as frames exist");
|
||||
return;
|
||||
};
|
||||
|
||||
self.queues.insert(manager.clone(), ScreencopyQueue::new());
|
||||
queue.push(screencopy);
|
||||
}
|
||||
|
||||
pub fn get_queue_mut(
|
||||
pub fn damage_tracker(
|
||||
&mut self,
|
||||
manager: &ZwlrScreencopyManagerV1,
|
||||
) -> Option<&mut ScreencopyQueue> {
|
||||
self.queues.get_mut(manager)
|
||||
) -> Option<&mut OutputDamageTracker> {
|
||||
let queue = self.queues.get_mut(manager)?;
|
||||
Some(&mut queue.damage_tracker)
|
||||
}
|
||||
|
||||
pub fn queues_mut(&mut self) -> impl Iterator<Item = &mut ScreencopyQueue> {
|
||||
self.queues.values_mut()
|
||||
pub fn remove_output(&mut self, output: &Output) {
|
||||
for queue in self.queues.values_mut() {
|
||||
queue.remove_output(output);
|
||||
}
|
||||
|
||||
self.cleanup_queues();
|
||||
}
|
||||
|
||||
pub fn queues(&self) -> impl Iterator<Item = &ScreencopyQueue> {
|
||||
self.queues.values()
|
||||
}
|
||||
|
||||
pub fn with_queues_mut(&mut self, mut f: impl FnMut(&mut ScreencopyQueue)) {
|
||||
for queue in self.queues.values_mut() {
|
||||
f(queue);
|
||||
}
|
||||
|
||||
self.cleanup_queues();
|
||||
}
|
||||
|
||||
fn cleanup_queues(&mut self) {
|
||||
self.queues
|
||||
.retain(|manager, queue| manager.is_alive() || !queue.is_empty());
|
||||
}
|
||||
|
||||
pub fn clear_expired_casts(&mut self) {
|
||||
for queue in self.queues.values_mut() {
|
||||
queue.clear_expired_cast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,14 +289,18 @@ where
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
dh: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: New<ZwlrScreencopyManagerV1>,
|
||||
_manager_state: &ScreencopyManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(manager, ());
|
||||
state.screencopy_state().bind(&manager);
|
||||
|
||||
let state = state.screencopy_state();
|
||||
let credentials = get_credentials_for_client(dh, client);
|
||||
let queue = ScreencopyQueue::new(credentials);
|
||||
state.queues.insert(manager.clone(), queue);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
|
||||
@@ -154,7 +317,7 @@ where
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
manager: &ZwlrScreencopyManagerV1,
|
||||
request: zwlr_screencopy_manager_v1::Request,
|
||||
@@ -273,13 +436,50 @@ where
|
||||
// Notify client that all supported buffers were enumerated.
|
||||
frame.buffer_done();
|
||||
}
|
||||
|
||||
let state = state.screencopy_state();
|
||||
let queue = state.queues.get_mut(manager).unwrap();
|
||||
queue.pending_frames.insert(frame);
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: wayland_backend::server::ClientId,
|
||||
manager: &ZwlrScreencopyManagerV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.screencopy_state();
|
||||
|
||||
let Some(queue) = state.queues.get_mut(manager) else {
|
||||
// This happened once. I'm really not sure how exactly though.
|
||||
//
|
||||
// I've dug into wayland-server and wayland-backend, and apparently there are a bunch
|
||||
// of places where calling destroyed() is delayed (even on a +1 ms timer). Then, it's
|
||||
// quite possible for some code to run cleanup_queues() *before* this destroyed()
|
||||
// handler, and delete the queue because the manager is no longer .is_alive() by then.
|
||||
// Then, queue will be None here.
|
||||
//
|
||||
// My attempts to reproduce this in a test have failed though. Perhaps it requires a
|
||||
// tricky timing condition where the client disconnects at some precise spot inside our
|
||||
// State::refresh_and_flush_clients() call.
|
||||
return;
|
||||
};
|
||||
|
||||
// Clean up the queue if this was the last object.
|
||||
if queue.is_empty() {
|
||||
state.queues.remove(manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for wlr-screencopy.
|
||||
pub trait ScreencopyHandler {
|
||||
/// Handle new screencopy request.
|
||||
///
|
||||
/// The handler must synchronously either ready/fail the screencopy, or submit it to the
|
||||
/// manager queue.
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy);
|
||||
|
||||
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState;
|
||||
}
|
||||
|
||||
@@ -405,6 +605,40 @@ where
|
||||
submitted: false,
|
||||
},
|
||||
);
|
||||
|
||||
// By this point the frame should've been either copied or failed or pushed to the queue,
|
||||
// so remove it from pending frames.
|
||||
let state = state.screencopy_state();
|
||||
let queue = state.queues.get_mut(manager).unwrap();
|
||||
queue.pending_frames.remove(frame);
|
||||
if queue.is_empty() && !manager.is_alive() {
|
||||
state.queues.remove(manager);
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: wayland_backend::server::ClientId,
|
||||
frame: &ZwlrScreencopyFrameV1,
|
||||
data: &ScreencopyFrameState,
|
||||
) {
|
||||
let ScreencopyFrameState::Pending { manager, .. } = data else {
|
||||
return;
|
||||
};
|
||||
|
||||
let state = state.screencopy_state();
|
||||
let Some(queue) = state.queues.get_mut(manager) else {
|
||||
// I think this can happen when we post_error() on a pending frame? Either way better
|
||||
// safe than sorry.
|
||||
return;
|
||||
};
|
||||
|
||||
queue.remove_frame(frame);
|
||||
|
||||
// Clean up the queue if this was the last object.
|
||||
if queue.is_empty() && !manager.is_alive() {
|
||||
state.queues.remove(manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
use smithay::backend::input::{
|
||||
Device, DeviceCapability, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode,
|
||||
UnusedEvent,
|
||||
};
|
||||
use smithay::delegate_virtual_keyboard_manager;
|
||||
use smithay::input::keyboard::xkb::ModMask;
|
||||
use smithay::input::keyboard::KeyboardHandle;
|
||||
use smithay::wayland::virtual_keyboard::VirtualKeyboardHandler;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct VirtualKeyboardInputBackend;
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct VirtualKeyboard;
|
||||
|
||||
impl Device for VirtualKeyboard {
|
||||
fn id(&self) -> String {
|
||||
String::from("virtual keyboard")
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
String::from("virtual keyboard")
|
||||
}
|
||||
|
||||
fn has_capability(&self, capability: DeviceCapability) -> bool {
|
||||
matches!(capability, DeviceCapability::Keyboard)
|
||||
}
|
||||
|
||||
fn usb_id(&self) -> Option<(u32, u32)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn syspath(&self) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualKeyboardKeyEvent {
|
||||
pub keycode: Keycode,
|
||||
pub state: KeyState,
|
||||
pub time: u32,
|
||||
}
|
||||
|
||||
impl Event<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualKeyboard {
|
||||
VirtualKeyboard
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardKeyEvent<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
|
||||
fn key_code(&self) -> Keycode {
|
||||
self.keycode
|
||||
}
|
||||
|
||||
fn state(&self) -> KeyState {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn count(&self) -> u32 {
|
||||
0 // Not used by niri
|
||||
}
|
||||
}
|
||||
|
||||
impl InputBackend for VirtualKeyboardInputBackend {
|
||||
type Device = VirtualKeyboard;
|
||||
|
||||
type KeyboardKeyEvent = VirtualKeyboardKeyEvent;
|
||||
type PointerAxisEvent = UnusedEvent;
|
||||
type PointerButtonEvent = UnusedEvent;
|
||||
type PointerMotionEvent = UnusedEvent;
|
||||
type PointerMotionAbsoluteEvent = UnusedEvent;
|
||||
|
||||
type GestureSwipeBeginEvent = UnusedEvent;
|
||||
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||
type GestureSwipeEndEvent = UnusedEvent;
|
||||
type GesturePinchBeginEvent = UnusedEvent;
|
||||
type GesturePinchUpdateEvent = UnusedEvent;
|
||||
type GesturePinchEndEvent = UnusedEvent;
|
||||
type GestureHoldBeginEvent = UnusedEvent;
|
||||
type GestureHoldEndEvent = UnusedEvent;
|
||||
|
||||
type TouchDownEvent = UnusedEvent;
|
||||
type TouchUpEvent = UnusedEvent;
|
||||
type TouchMotionEvent = UnusedEvent;
|
||||
type TouchCancelEvent = UnusedEvent;
|
||||
type TouchFrameEvent = UnusedEvent;
|
||||
type TabletToolAxisEvent = UnusedEvent;
|
||||
type TabletToolProximityEvent = UnusedEvent;
|
||||
type TabletToolTipEvent = UnusedEvent;
|
||||
type TabletToolButtonEvent = UnusedEvent;
|
||||
|
||||
type SwitchToggleEvent = UnusedEvent;
|
||||
|
||||
type SpecialEvent = UnusedEvent;
|
||||
}
|
||||
|
||||
impl VirtualKeyboardHandler for State {
|
||||
fn on_keyboard_event(
|
||||
&mut self,
|
||||
keycode: Keycode,
|
||||
state: KeyState,
|
||||
time: u32,
|
||||
_keyboard: KeyboardHandle<Self>,
|
||||
) {
|
||||
// The virtual keyboard impl in Smithay changes the keymap, so we'll need to reset it on
|
||||
// the next real keyboard event.
|
||||
self.niri.reset_keymap = true;
|
||||
|
||||
let event = VirtualKeyboardKeyEvent {
|
||||
keycode,
|
||||
state,
|
||||
time,
|
||||
};
|
||||
self.process_input_event(InputEvent::<VirtualKeyboardInputBackend>::Keyboard { event });
|
||||
}
|
||||
|
||||
// We handle modifiers when the key event is sent.
|
||||
fn on_keyboard_modifiers(
|
||||
&mut self,
|
||||
_depressed_mods: ModMask,
|
||||
_latched_mods: ModMask,
|
||||
_locked_mods: ModMask,
|
||||
_keyboard: KeyboardHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::{
|
||||
@@ -7,12 +8,14 @@ use niri_config::{
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
use super::shader_element::ShaderRenderElement;
|
||||
use super::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
use crate::render_helpers::renderer::AsGlesFrame as _;
|
||||
|
||||
/// Renders a wide variety of borders and border parts.
|
||||
///
|
||||
@@ -197,7 +200,7 @@ impl BorderRenderElement {
|
||||
None,
|
||||
scale,
|
||||
alpha,
|
||||
vec![
|
||||
Rc::new([
|
||||
Uniform::new("colorspace", colorspace),
|
||||
Uniform::new("hue_interpolation", hue_interpolation),
|
||||
Uniform::new("color_from", color_from.to_array_unpremul()),
|
||||
@@ -209,7 +212,7 @@ impl BorderRenderElement {
|
||||
Uniform::new("geo_size", geo_size.to_array()),
|
||||
Uniform::new("outer_radius", <[f32; 4]>::from(corner_radius)),
|
||||
Uniform::new("border_width", border_width),
|
||||
],
|
||||
]),
|
||||
HashMap::new(),
|
||||
);
|
||||
}
|
||||
@@ -283,7 +286,17 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
let _span = tracy_client::span!("BorderRenderElement::draw");
|
||||
frame.with_gpu_span(gpu_span_location!("BorderRenderElement::draw"), |frame| {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.inner,
|
||||
frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
@@ -300,7 +313,9 @@ impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
let frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
|
||||
program: GlesTexProgram,
|
||||
corner_radius: CornerRadius,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -36,23 +36,34 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
program: GlesTexProgram,
|
||||
corner_radius: CornerRadius,
|
||||
) -> Self {
|
||||
let elem_geo = elem.geometry(scale);
|
||||
Self {
|
||||
inner: elem,
|
||||
program,
|
||||
corner_radius,
|
||||
geometry,
|
||||
scale: scale.x as f32,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_uniforms(&self) -> Vec<Uniform<'static>> {
|
||||
let scale = Scale::from(f64::from(self.scale));
|
||||
let elem_geo = self.inner.geometry(scale);
|
||||
|
||||
let elem_geo_loc = Vec2::new(elem_geo.loc.x as f32, elem_geo.loc.y as f32);
|
||||
let elem_geo_size = Vec2::new(elem_geo.size.w as f32, elem_geo.size.h as f32);
|
||||
|
||||
let geo = geometry.to_physical_precise_round(scale);
|
||||
let geo = self.geometry.to_physical_precise_round(scale);
|
||||
let geo_loc = Vec2::new(geo.loc.x, geo.loc.y);
|
||||
let geo_size = Vec2::new(geo.size.w, geo.size.h);
|
||||
|
||||
let buf_size = elem.buffer_size();
|
||||
let buf_size = self.inner.buffer_size();
|
||||
let buf_size = Vec2::new(buf_size.w as f32, buf_size.h as f32);
|
||||
|
||||
let view = elem.view();
|
||||
let view = self.inner.view();
|
||||
let src_loc = Vec2::new(view.src.loc.x as f32, view.src.loc.y as f32);
|
||||
let src_size = Vec2::new(view.src.size.w as f32, view.src.size.h as f32);
|
||||
|
||||
let transform = elem.transform();
|
||||
let transform = self.inner.transform();
|
||||
// HACK: ??? for some reason flipped ones are fine.
|
||||
let transform = match transform {
|
||||
Transform::_90 => Transform::_270,
|
||||
@@ -70,20 +81,14 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
* Mat3::from_scale(buf_size / src_size)
|
||||
* Mat3::from_translation(-src_loc / buf_size);
|
||||
|
||||
let uniforms = vec![
|
||||
Uniform::new("niri_scale", scale.x as f32),
|
||||
Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
|
||||
mat3_uniform("input_to_geo", input_to_geo),
|
||||
];
|
||||
let geo_size = (self.geometry.size.w as f32, self.geometry.size.h as f32);
|
||||
|
||||
Self {
|
||||
inner: elem,
|
||||
program,
|
||||
corner_radius,
|
||||
geometry,
|
||||
uniforms,
|
||||
}
|
||||
vec![
|
||||
Uniform::new("niri_scale", self.scale),
|
||||
Uniform::new("geo_size", geo_size),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
||||
mat3_uniform("input_to_geo", input_to_geo),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn shader(renderer: &mut R) -> Option<&GlesTexProgram> {
|
||||
@@ -224,7 +229,7 @@ impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
frame.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||
frame.override_default_tex_program(self.program.clone(), self.compute_uniforms());
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
frame.clear_tex_program_override();
|
||||
Ok(())
|
||||
@@ -250,7 +255,7 @@ impl<'render> RenderElement<TtyRenderer<'render>>
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
frame
|
||||
.as_gles_frame()
|
||||
.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||
.override_default_tex_program(self.program.clone(), self.compute_uniforms());
|
||||
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
frame.as_gles_frame().clear_tex_program_override();
|
||||
Ok(())
|
||||
|
||||
+32
-41
@@ -8,54 +8,45 @@ use super::renderer::NiriRenderer;
|
||||
use super::solid_color::SolidColorRenderElement;
|
||||
use crate::niri::OutputRenderElements;
|
||||
|
||||
pub fn draw_opaque_regions<R: NiriRenderer>(
|
||||
elements: &mut Vec<OutputRenderElements<R>>,
|
||||
pub fn push_opaque_regions<R: NiriRenderer>(
|
||||
elem: &OutputRenderElements<R>,
|
||||
scale: Scale<f64>,
|
||||
push: &mut dyn FnMut(OutputRenderElements<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("draw_opaque_regions");
|
||||
// HACK
|
||||
if format!("{elem:?}").contains("ExtraDamage") {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i < elements.len() {
|
||||
let elem = &elements[i];
|
||||
i += 1;
|
||||
let geo = elem.geometry(scale);
|
||||
let mut opaque = elem.opaque_regions(scale).to_vec();
|
||||
|
||||
// HACK
|
||||
if format!("{elem:?}").contains("ExtraDamage") {
|
||||
continue;
|
||||
}
|
||||
for rect in &mut opaque {
|
||||
rect.loc += geo.loc;
|
||||
}
|
||||
|
||||
let geo = elem.geometry(scale);
|
||||
let mut opaque = elem.opaque_regions(scale).to_vec();
|
||||
let semitransparent = geo.subtract_rects(opaque.iter().copied());
|
||||
|
||||
for rect in &mut opaque {
|
||||
rect.loc += geo.loc;
|
||||
}
|
||||
for rect in opaque {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
Color32F::from([0., 0., 0.2, 0.2]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
push(color.into());
|
||||
}
|
||||
|
||||
let semitransparent = geo.subtract_rects(opaque.iter().copied());
|
||||
|
||||
for rect in opaque {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
Color32F::from([0., 0., 0.2, 0.2]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
for rect in semitransparent {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
Color32F::from([0.3, 0., 0., 0.3]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
|
||||
i += 1;
|
||||
}
|
||||
for rect in semitransparent {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
Color32F::from([0.3, 0., 0., 0.3]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
push(color.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::render_helpers::shaders::Shaders;
|
||||
pub struct GradientFadeTextureRenderElement {
|
||||
inner: TextureRenderElement<GlesTexture>,
|
||||
program: GradientFadeShader,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
cutoff: (f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -33,11 +33,10 @@ impl GradientFadeTextureRenderElement {
|
||||
// Texture is displayed full-size, no cutoff necessary.
|
||||
(1., 1.)
|
||||
};
|
||||
let uniforms = vec![Uniform::new("cutoff", cutoff)];
|
||||
Self {
|
||||
inner: texture,
|
||||
program,
|
||||
uniforms,
|
||||
cutoff,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +97,8 @@ impl RenderElement<GlesRenderer> for GradientFadeTextureRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
frame.override_default_tex_program(self.program.0.clone(), self.uniforms.clone());
|
||||
let uniforms = vec![Uniform::new("cutoff", self.cutoff)];
|
||||
frame.override_default_tex_program(self.program.0.clone(), uniforms);
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
frame.clear_tex_program_override();
|
||||
Ok(())
|
||||
|
||||
@@ -5,7 +5,7 @@ use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTarget, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
@@ -58,13 +58,6 @@ pub struct BakedBuffer<B> {
|
||||
pub dst: Option<Size<i32, Logical>>,
|
||||
}
|
||||
|
||||
/// Render elements split into normal and popup.
|
||||
#[derive(Debug)]
|
||||
pub struct SplitElements<E> {
|
||||
pub normal: Vec<E>,
|
||||
pub popups: Vec<E>,
|
||||
}
|
||||
|
||||
pub trait ToRenderElement {
|
||||
type RenderElement;
|
||||
|
||||
@@ -87,41 +80,6 @@ impl RenderTarget {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Default for SplitElements<E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
normal: Vec::new(),
|
||||
popups: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> IntoIterator for SplitElements<E> {
|
||||
type Item = E;
|
||||
type IntoIter = std::iter::Chain<std::vec::IntoIter<E>, std::vec::IntoIter<E>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.popups.into_iter().chain(self.normal)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> SplitElements<E> {
|
||||
pub fn iter(&self) -> std::iter::Chain<std::slice::Iter<'_, E>, std::slice::Iter<'_, E>> {
|
||||
self.popups.iter().chain(&self.normal)
|
||||
}
|
||||
|
||||
pub fn into_vec(self) -> Vec<E> {
|
||||
let Self { normal, mut popups } = self;
|
||||
popups.extend(normal);
|
||||
popups
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: SplitElements<E>) {
|
||||
self.popups.extend(other.popups);
|
||||
self.normal.extend(other.normal);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
|
||||
type RenderElement = PrimaryGpuTextureRenderElement;
|
||||
|
||||
@@ -160,7 +118,7 @@ impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
|
||||
|
||||
pub fn encompassing_geo(
|
||||
scale: Scale<f64>,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
elements: impl Iterator<Item = impl Element>,
|
||||
) -> Rectangle<i32, Physical> {
|
||||
elements
|
||||
.map(|ele| ele.geometry(scale))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
@@ -6,6 +7,7 @@ use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, Unde
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::backend::renderer::Texture as _;
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::{AsGlesFrame, NiriRenderer};
|
||||
@@ -90,7 +92,7 @@ impl ResizeRenderElement {
|
||||
None,
|
||||
scale.x,
|
||||
result_alpha,
|
||||
vec![
|
||||
Rc::new([
|
||||
mat3_uniform("niri_input_to_curr_geo", input_to_curr_geo),
|
||||
mat3_uniform("niri_curr_geo_to_prev_geo", curr_geo_to_prev_geo),
|
||||
mat3_uniform("niri_curr_geo_to_next_geo", curr_geo_to_next_geo),
|
||||
@@ -101,7 +103,7 @@ impl ResizeRenderElement {
|
||||
Uniform::new("niri_clamped_progress", clamped_progress),
|
||||
Uniform::new("niri_corner_radius", <[f32; 4]>::from(corner_radius)),
|
||||
Uniform::new("niri_clip_to_geometry", clip_to_geometry),
|
||||
],
|
||||
]),
|
||||
HashMap::from([
|
||||
(String::from("niri_tex_prev"), texture_prev),
|
||||
(String::from("niri_tex_next"), texture_next),
|
||||
@@ -170,8 +172,10 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)?;
|
||||
Ok(())
|
||||
let _span = tracy_client::span!("ResizeRenderElement::draw");
|
||||
frame.with_gpu_span(gpu_span_location!("ResizeRenderElement::draw"), |frame| {
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)
|
||||
})
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
@@ -188,8 +192,8 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
let frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct ShaderRenderElement {
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
additional_uniforms: Vec<Uniform<'static>>,
|
||||
additional_uniforms: Rc<[Uniform<'static>]>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
kind: Kind,
|
||||
}
|
||||
@@ -185,7 +185,7 @@ impl ShaderRenderElement {
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
additional_uniforms: Vec<Uniform<'static>>,
|
||||
additional_uniforms: Rc<[Uniform<'static>]>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
@@ -212,7 +212,7 @@ impl ShaderRenderElement {
|
||||
opaque_regions: vec![],
|
||||
scale: 1.,
|
||||
alpha: 1.,
|
||||
additional_uniforms: vec![],
|
||||
additional_uniforms: Rc::new([]),
|
||||
textures: HashMap::new(),
|
||||
kind,
|
||||
}
|
||||
@@ -228,7 +228,7 @@ impl ShaderRenderElement {
|
||||
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
uniforms: Rc<[Uniform<'static>]>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
) {
|
||||
self.area.size = size;
|
||||
@@ -294,6 +294,8 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
let _span = tracy_client::span!("ShaderRenderElement::draw");
|
||||
|
||||
let frame = frame.as_gles_frame();
|
||||
|
||||
let Some(shader) = Shaders::get_from_frame(frame).program(self.program) else {
|
||||
@@ -373,7 +375,8 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
let has_tint = frame.debug_flags().contains(DebugFlags::TINT);
|
||||
|
||||
// render
|
||||
frame.with_context(move |gl| -> Result<(), GlesError> {
|
||||
let span_loc = smithay::gpu_span_location!("draw shader");
|
||||
frame.with_profiled_context(span_loc, move |gl| -> Result<(), GlesError> {
|
||||
let program = if has_debug {
|
||||
&shader.0.debug
|
||||
} else {
|
||||
@@ -425,7 +428,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
gl.Uniform1f(shader.0.uniform_tint, tint);
|
||||
}
|
||||
|
||||
for uniform in &self.additional_uniforms {
|
||||
for uniform in &*self.additional_uniforms {
|
||||
let desc =
|
||||
program
|
||||
.additional_uniforms
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::{Color, CornerRadius};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
use super::shader_element::ShaderRenderElement;
|
||||
use super::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
use crate::render_helpers::renderer::AsGlesFrame as _;
|
||||
|
||||
/// Renders a rounded rectangle shadow.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -153,7 +156,7 @@ impl ShadowRenderElement {
|
||||
None,
|
||||
scale,
|
||||
alpha,
|
||||
vec![
|
||||
Rc::new([
|
||||
Uniform::new("shadow_color", color.to_array_premul()),
|
||||
Uniform::new("sigma", sigma),
|
||||
mat3_uniform("input_to_geo", input_to_geo),
|
||||
@@ -165,7 +168,7 @@ impl ShadowRenderElement {
|
||||
"window_corner_radius",
|
||||
<[f32; 4]>::from(window_corner_radius),
|
||||
),
|
||||
],
|
||||
]),
|
||||
HashMap::new(),
|
||||
);
|
||||
}
|
||||
@@ -244,7 +247,17 @@ impl RenderElement<GlesRenderer> for ShadowRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
let _span = tracy_client::span!("ShadowRenderElement::draw");
|
||||
frame.with_gpu_span(gpu_span_location!("ShadowRenderElement::draw"), |frame| {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.inner,
|
||||
frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
@@ -261,7 +274,9 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ShadowRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
let frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(
|
||||
|
||||
@@ -49,7 +49,7 @@ where
|
||||
) -> Option<&(GlesTexture, Rectangle<i32, Physical>)> {
|
||||
if target.should_block_out(self.block_out_from) {
|
||||
self.blocked_out_texture.get_or_init(|| {
|
||||
let _span = tracy_client::span!("RenderSnapshot::Texture");
|
||||
let _span = tracy_client::span!("RenderSnapshot::texture");
|
||||
|
||||
let elements: Vec<_> = self
|
||||
.blocked_out_contents
|
||||
@@ -75,7 +75,7 @@ where
|
||||
})
|
||||
} else {
|
||||
self.texture.get_or_init(|| {
|
||||
let _span = tracy_client::span!("RenderSnapshot::Texture");
|
||||
let _span = tracy_client::span!("RenderSnapshot::texture");
|
||||
|
||||
let elements: Vec<_> = self
|
||||
.contents
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::{import_surface, RendererSurfaceStateUserData};
|
||||
use smithay::backend::renderer::Renderer as _;
|
||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point};
|
||||
use smithay::utils::{Logical, Physical, Point, Scale};
|
||||
use smithay::wayland::compositor::{with_surface_tree_downward, TraversalAction};
|
||||
|
||||
use super::texture::TextureBuffer;
|
||||
@@ -78,3 +80,67 @@ pub fn render_snapshot_from_surface_tree(
|
||||
|_, _, _| true,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn push_elements_from_surface_tree<R>(
|
||||
renderer: &mut R,
|
||||
surface: &WlSurface,
|
||||
// Fractional scale expects surface buffers to be aligned to physical pixels.
|
||||
location: Point<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
kind: Kind,
|
||||
push: &mut dyn FnMut(WaylandSurfaceRenderElement<R>),
|
||||
) where
|
||||
R: Renderer + ImportAll,
|
||||
R::TextureId: Clone + 'static,
|
||||
{
|
||||
let _span = tracy_client::span!("push_elements_from_surface_tree");
|
||||
|
||||
let location = location.to_f64();
|
||||
|
||||
with_surface_tree_downward(
|
||||
surface,
|
||||
location,
|
||||
|_, states, location| {
|
||||
let mut location = *location;
|
||||
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||
|
||||
if let Some(data) = data {
|
||||
if let Some(view) = data.lock().unwrap().view() {
|
||||
location += view.offset.to_f64().to_physical(scale);
|
||||
TraversalAction::DoChildren(location)
|
||||
} else {
|
||||
TraversalAction::SkipChildren
|
||||
}
|
||||
} else {
|
||||
TraversalAction::SkipChildren
|
||||
}
|
||||
},
|
||||
|surface, states, location| {
|
||||
let mut location = *location;
|
||||
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||
|
||||
if let Some(data) = data {
|
||||
let has_view = if let Some(view) = data.lock().unwrap().view() {
|
||||
location += view.offset.to_f64().to_physical(scale);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if has_view {
|
||||
match WaylandSurfaceRenderElement::from_surface(
|
||||
renderer, surface, states, location, alpha, kind,
|
||||
) {
|
||||
Ok(Some(surface)) => push(surface),
|
||||
Ok(None) => {} // surface is not mapped
|
||||
Err(err) => {
|
||||
warn!("failed to import surface: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|_, _, _| true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use calloop::LoopHandle;
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::GbmDevice;
|
||||
use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Point, Scale, Size};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri, StreamTargetId};
|
||||
use crate::niri::{CastTarget, Niri, OutputRenderElements, PointerRenderElements, State};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::{get_monotonic_time, CastSessionId, CastStreamId};
|
||||
use crate::window::mapped::{MappedId, WindowCastRenderElements};
|
||||
|
||||
mod pw_utils;
|
||||
use pw_utils::{Cast, CastSizeChange, CursorData, PipeWire, PwToNiri};
|
||||
|
||||
pub struct Screencasting {
|
||||
pub casts: Vec<Cast>,
|
||||
|
||||
/// Dynamic-target casts waiting for their first target to start.
|
||||
pub pending_dynamic_casts: Vec<PendingCast>,
|
||||
|
||||
pub pw_to_niri: calloop::channel::Sender<PwToNiri>,
|
||||
|
||||
/// Screencast output for each mapped window.
|
||||
pub mapped_cast_output: HashMap<Window, Output>,
|
||||
|
||||
/// Window ID for the "dynamic cast" special window for the xdp-gnome picker.
|
||||
pub dynamic_cast_id_for_portal: MappedId,
|
||||
|
||||
// Drop PipeWire last, and specifically after casts, to prevent a double-free (yay).
|
||||
pub pipewire: Option<PipeWire>,
|
||||
}
|
||||
|
||||
/// A screencast request that hasn't been started yet.
|
||||
pub struct PendingCast {
|
||||
pub session_id: CastSessionId,
|
||||
pub stream_id: CastStreamId,
|
||||
pub cursor_mode: CursorMode,
|
||||
pub signal_ctx: SignalEmitter<'static>,
|
||||
}
|
||||
|
||||
impl Screencasting {
|
||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> Self {
|
||||
let pw_to_niri = {
|
||||
let (pw_to_niri, from_pipewire) = calloop::channel::channel();
|
||||
event_loop
|
||||
.insert_source(from_pipewire, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
pw_to_niri
|
||||
};
|
||||
|
||||
Self {
|
||||
casts: vec![],
|
||||
pending_dynamic_casts: vec![],
|
||||
pw_to_niri,
|
||||
mapped_cast_output: HashMap::new(),
|
||||
dynamic_cast_id_for_portal: MappedId::next(),
|
||||
pipewire: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn prepare_pw_cast(&mut self) -> anyhow::Result<(GbmDevice<DrmDeviceFd>, FormatSet)> {
|
||||
let gbm = self
|
||||
.backend
|
||||
.gbm_device()
|
||||
.context("no GBM device available")?;
|
||||
|
||||
// Ensure PipeWire is initialized.
|
||||
if self.niri.casting.pipewire.is_none() {
|
||||
let pw = PipeWire::new(
|
||||
self.niri.event_loop.clone(),
|
||||
self.niri.casting.pw_to_niri.clone(),
|
||||
)
|
||||
.context("error initializing PipeWire")?;
|
||||
self.niri.casting.pipewire = Some(pw);
|
||||
}
|
||||
|
||||
let mut render_formats = self
|
||||
.backend
|
||||
.with_primary_renderer(|renderer| {
|
||||
renderer.egl_context().dmabuf_render_formats().clone()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
{
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.force_pipewire_invalid_modifier {
|
||||
render_formats = render_formats
|
||||
.into_iter()
|
||||
.filter(|f| f.modifier == Modifier::Invalid)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
Ok((gbm, render_formats))
|
||||
}
|
||||
|
||||
pub fn on_pw_msg(&mut self, msg: PwToNiri) {
|
||||
match msg {
|
||||
PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
|
||||
PwToNiri::Redraw { stream_id } => self.redraw_cast(stream_id),
|
||||
PwToNiri::FatalError => {
|
||||
warn!("stopping PipeWire due to fatal error");
|
||||
let casting = &mut self.niri.casting;
|
||||
if let Some(pw) = casting.pipewire.take() {
|
||||
let mut ids = HashSet::new();
|
||||
for cast in &casting.pending_dynamic_casts {
|
||||
ids.insert(cast.session_id);
|
||||
}
|
||||
for cast in &casting.casts {
|
||||
ids.insert(cast.session_id);
|
||||
}
|
||||
for id in ids {
|
||||
self.niri.stop_cast(id);
|
||||
}
|
||||
self.niri.event_loop.remove(pw.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_cast(&mut self, stream_id: CastStreamId) {
|
||||
let _span = tracy_client::span!("State::redraw_cast");
|
||||
|
||||
let casts = &mut self.niri.casting.casts;
|
||||
let Some(idx) = casts.iter().position(|cast| cast.stream_id == stream_id) else {
|
||||
warn!("cast to redraw is missing");
|
||||
return;
|
||||
};
|
||||
let cast = &mut casts[idx];
|
||||
|
||||
let id = match &cast.target {
|
||||
CastTarget::Nothing => {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if cast.dequeue_buffer_and_clear(renderer) {
|
||||
cast.last_frame_time = get_monotonic_time();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
CastTarget::Output { output, .. } => {
|
||||
if let Some(output) = output.upgrade() {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
CastTarget::Window { id } => *id,
|
||||
};
|
||||
|
||||
// Lack of partial borrowing strikes again...
|
||||
let mut casts = mem::take(&mut self.niri.casting.casts);
|
||||
let cast = &mut casts[idx];
|
||||
let mut stop = false;
|
||||
// Use a loop {} so we can break instead of early-return.
|
||||
#[allow(clippy::never_loop)]
|
||||
loop {
|
||||
let mut windows = self.niri.layout.windows();
|
||||
let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == id) else {
|
||||
break;
|
||||
};
|
||||
|
||||
// Use the cached output since it will be present even if the output was
|
||||
// currently disconnected.
|
||||
let Some(output) = self.niri.casting.mapped_cast_output.get(&mapped.window) else {
|
||||
break;
|
||||
};
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
let bbox = mapped
|
||||
.window
|
||||
.bbox_with_popups()
|
||||
.to_physical_precise_up(scale);
|
||||
|
||||
match cast.ensure_size(bbox.size) {
|
||||
Ok(CastSizeChange::Ready) => (),
|
||||
Ok(CastSizeChange::Pending) => break,
|
||||
Err(err) => {
|
||||
warn!("error updating stream size, stopping screencast: {err:?}");
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
let mut elements = Vec::new();
|
||||
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
|
||||
elements.push(CastRenderElement::from(elem))
|
||||
});
|
||||
|
||||
let mut pointer_elements = Vec::new();
|
||||
let mut pointer_location = Point::default();
|
||||
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
if let Some((pointer_pos, win_pos)) =
|
||||
self.niri.pointer_pos_for_window_cast(mapped)
|
||||
{
|
||||
// Pointer location must be relative to the screencast buffer.
|
||||
// - win_pos is the position of the main window surface in output-local
|
||||
// coordinates
|
||||
// - bbox.loc moves us relative to the screencast buffer
|
||||
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
|
||||
let output_pos =
|
||||
self.niri.global_space.output_geometry(output).unwrap().loc;
|
||||
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
|
||||
|
||||
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
|
||||
self.niri.render_pointer(renderer, output, &mut |elem| {
|
||||
let elem =
|
||||
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
|
||||
pointer_elements.push(CastRenderElement::from(elem));
|
||||
});
|
||||
}
|
||||
}
|
||||
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
|
||||
|
||||
if cast.dequeue_buffer_and_render(
|
||||
renderer,
|
||||
&elements,
|
||||
&cursor_data,
|
||||
bbox.size,
|
||||
scale,
|
||||
) {
|
||||
cast.last_frame_time = get_monotonic_time();
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
let session_id = cast.session_id;
|
||||
self.niri.casting.casts = casts;
|
||||
|
||||
if stop {
|
||||
self.niri.stop_cast(session_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dynamic_cast_target(&mut self, target: CastTarget) {
|
||||
let _span = tracy_client::span!("State::set_dynamic_cast_target");
|
||||
|
||||
let mut refresh = None;
|
||||
match &target {
|
||||
// Leave refresh as is when clearing. Chances are, the next refresh will match it,
|
||||
// then we'll avoid reconfiguring.
|
||||
CastTarget::Nothing => (),
|
||||
CastTarget::Output { output, .. } => {
|
||||
if let Some(output) = output.upgrade() {
|
||||
refresh = Some(output.current_mode().unwrap().refresh as u32);
|
||||
}
|
||||
}
|
||||
CastTarget::Window { id } => {
|
||||
let mut windows = self.niri.layout.windows();
|
||||
if let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == *id) {
|
||||
if let Some(output) = self.niri.casting.mapped_cast_output.get(&mapped.window) {
|
||||
refresh = Some(output.current_mode().unwrap().refresh as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_redraw = Vec::new();
|
||||
let mut to_stop = Vec::new();
|
||||
for cast in &mut self.niri.casting.casts {
|
||||
if !cast.dynamic_target {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(refresh) = refresh {
|
||||
if let Err(err) = cast.set_refresh(refresh) {
|
||||
warn!("error changing cast FPS: {err:?}");
|
||||
to_stop.push(cast.session_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cast.target = target.clone();
|
||||
to_redraw.push(cast.stream_id);
|
||||
}
|
||||
|
||||
for id in to_redraw {
|
||||
self.redraw_cast(id);
|
||||
}
|
||||
|
||||
// Start any pending dynamic casts if we have a real target.
|
||||
if !matches!(target, CastTarget::Nothing) {
|
||||
self.start_pending_dynamic_casts(&target);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_pending_dynamic_casts(&mut self, target: &CastTarget) {
|
||||
let pending = &self.niri.casting.pending_dynamic_casts;
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
debug!("starting {} pending dynamic cast(s)", pending.len());
|
||||
|
||||
let _span = tracy_client::span!("State::start_pending_dynamic_casts");
|
||||
|
||||
// We don't stop dynamic casts on missing output/window.
|
||||
let (size, refresh) = match target {
|
||||
CastTarget::Nothing => panic!("dynamic cast starting target must not be Nothing"),
|
||||
CastTarget::Output { output, .. } => {
|
||||
let Some(output) = output.upgrade() else {
|
||||
return;
|
||||
};
|
||||
cast_params_for_output(&output)
|
||||
}
|
||||
CastTarget::Window { id } => {
|
||||
let Some((size, refresh)) = self.niri.cast_params_for_window(*id) else {
|
||||
return;
|
||||
};
|
||||
(size, refresh)
|
||||
}
|
||||
};
|
||||
|
||||
let (gbm, render_formats) = match self.prepare_pw_cast() {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!("error starting pending screencasts: {err:?}");
|
||||
let mut ids = HashSet::new();
|
||||
for pending in self.niri.casting.pending_dynamic_casts.drain(..) {
|
||||
ids.insert(pending.session_id);
|
||||
}
|
||||
for id in ids {
|
||||
self.niri.stop_cast(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pw = self.niri.casting.pipewire.as_ref().unwrap();
|
||||
|
||||
// Alpha is always true since the dynamic target can change between window & output.
|
||||
let alpha = true;
|
||||
|
||||
// Start each pending cast.
|
||||
let mut to_stop = HashSet::new();
|
||||
for pending in self.niri.casting.pending_dynamic_casts.drain(..) {
|
||||
let res = pw.start_cast(
|
||||
gbm.clone(),
|
||||
render_formats.clone(),
|
||||
pending.session_id,
|
||||
pending.stream_id,
|
||||
target.clone(),
|
||||
size,
|
||||
refresh,
|
||||
alpha,
|
||||
pending.cursor_mode,
|
||||
pending.signal_ctx,
|
||||
);
|
||||
match res {
|
||||
Ok(mut cast) => {
|
||||
cast.dynamic_target = true;
|
||||
self.niri.casting.casts.push(cast);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error starting pending screencast: {err:?}");
|
||||
to_stop.insert(pending.session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for session_id in to_stop {
|
||||
self.niri.stop_cast(session_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) {
|
||||
match msg {
|
||||
ScreenCastToNiri::StartCast {
|
||||
session_id,
|
||||
stream_id,
|
||||
target,
|
||||
cursor_mode,
|
||||
signal_ctx,
|
||||
} => {
|
||||
let _span = tracy_client::span!("StartCast");
|
||||
let _span = debug_span!("StartCast", %session_id, %stream_id).entered();
|
||||
|
||||
let (target, size, refresh, alpha) = match target {
|
||||
StreamTargetId::Output { name } => {
|
||||
let global_space = &self.niri.global_space;
|
||||
let output = global_space.outputs().find(|out| out.name() == name);
|
||||
let Some(output) = output else {
|
||||
warn!("error starting screencast: requested output is missing");
|
||||
self.niri.stop_cast(session_id);
|
||||
return;
|
||||
};
|
||||
|
||||
let (size, refresh) = cast_params_for_output(output);
|
||||
(CastTarget::output(output), size, refresh, false)
|
||||
}
|
||||
StreamTargetId::Window { id }
|
||||
if id == self.niri.casting.dynamic_cast_id_for_portal.get() =>
|
||||
{
|
||||
debug!("delaying dynamic cast until target is set");
|
||||
self.niri.casting.pending_dynamic_casts.push(PendingCast {
|
||||
session_id,
|
||||
stream_id,
|
||||
cursor_mode,
|
||||
signal_ctx,
|
||||
});
|
||||
return;
|
||||
}
|
||||
StreamTargetId::Window { id } => {
|
||||
let Some((size, refresh)) = self.niri.cast_params_for_window(id) else {
|
||||
warn!("error starting screencast: requested window is missing");
|
||||
self.niri.stop_cast(session_id);
|
||||
return;
|
||||
};
|
||||
(CastTarget::Window { id }, size, refresh, true)
|
||||
}
|
||||
};
|
||||
|
||||
let (gbm, render_formats) = match self.prepare_pw_cast() {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!("error starting screencast: {err:?}");
|
||||
self.niri.stop_cast(session_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pw = self.niri.casting.pipewire.as_ref().unwrap();
|
||||
|
||||
let res = pw.start_cast(
|
||||
gbm,
|
||||
render_formats,
|
||||
session_id,
|
||||
stream_id,
|
||||
target,
|
||||
size,
|
||||
refresh,
|
||||
alpha,
|
||||
cursor_mode,
|
||||
signal_ctx,
|
||||
);
|
||||
match res {
|
||||
Ok(cast) => {
|
||||
self.niri.casting.casts.push(cast);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error starting screencast: {err:?}");
|
||||
self.niri.stop_cast(session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Niri {
|
||||
pub fn refresh_mapped_cast_window_rules(&mut self) {
|
||||
// O(N^2) but should be fine since there aren't many casts usually.
|
||||
self.layout.with_windows_mut(|mapped, _| {
|
||||
let id = mapped.id().get();
|
||||
// Find regardless of cast.is_active.
|
||||
let value = self
|
||||
.casting
|
||||
.casts
|
||||
.iter()
|
||||
.any(|cast| cast.target == (CastTarget::Window { id }));
|
||||
mapped.set_is_window_cast_target(value);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh_mapped_cast_outputs(&mut self) {
|
||||
let mut seen = HashSet::new();
|
||||
let mut output_changed = vec![];
|
||||
|
||||
self.layout.with_windows(|mapped, output, _, _| {
|
||||
seen.insert(mapped.window.clone());
|
||||
|
||||
let Some(output) = output else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.casting.mapped_cast_output.entry(mapped.window.clone()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if entry.get() != output {
|
||||
entry.insert(output.clone());
|
||||
output_changed.push((mapped.id(), output.clone()));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(output.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.casting
|
||||
.mapped_cast_output
|
||||
.retain(|win, _| seen.contains(win));
|
||||
|
||||
let mut to_stop = vec![];
|
||||
for (id, out) in output_changed {
|
||||
let refresh = out.current_mode().unwrap().refresh as u32;
|
||||
let target = CastTarget::Window { id: id.get() };
|
||||
for cast in self
|
||||
.casting
|
||||
.casts
|
||||
.iter_mut()
|
||||
.filter(|cast| cast.target == target)
|
||||
{
|
||||
if let Err(err) = cast.set_refresh(refresh) {
|
||||
warn!("error changing cast FPS: {err:?}");
|
||||
to_stop.push(cast.session_id);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for session_id in to_stop {
|
||||
self.stop_cast(session_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_for_screen_cast(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
target_presentation_time: Duration,
|
||||
) {
|
||||
let _span = tracy_client::span!("Niri::render_for_screen_cast");
|
||||
|
||||
let weak = output.downgrade();
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
|
||||
let mut elements = Vec::new();
|
||||
let mut pointer = Vec::new();
|
||||
let mut cursor_data = None;
|
||||
|
||||
let mut casts_to_stop = vec![];
|
||||
|
||||
let mut casts = mem::take(&mut self.casting.casts);
|
||||
for cast in &mut casts {
|
||||
if !cast.is_active() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !cast.target.matches_output(&weak) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match cast.ensure_size(size) {
|
||||
Ok(CastSizeChange::Ready) => (),
|
||||
Ok(CastSizeChange::Pending) => continue,
|
||||
Err(err) => {
|
||||
warn!("error updating stream size, stopping screencast: {err:?}");
|
||||
casts_to_stop.push(cast.session_id);
|
||||
}
|
||||
}
|
||||
|
||||
if cast.check_time_and_schedule(output, target_presentation_time) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if cursor_data.is_none() {
|
||||
// FIXME: support debug draw opaque regions.
|
||||
self.render_inner(
|
||||
renderer,
|
||||
output,
|
||||
false,
|
||||
RenderTarget::Screencast,
|
||||
&mut |elem| elements.push(elem.into()),
|
||||
);
|
||||
|
||||
let mut pointer_pos = Point::default();
|
||||
if self.pointer_visibility.is_visible() {
|
||||
let output_geo = self.global_space.output_geometry(output).unwrap().to_f64();
|
||||
let pointer_loc = self
|
||||
.tablet_cursor_location
|
||||
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
|
||||
// Only render when the pointer is within the output. Otherwise, it will
|
||||
// happily appear anywhere outside the output video source in OBS.
|
||||
if output_geo.contains(pointer_loc) {
|
||||
pointer_pos = pointer_loc - output_geo.loc;
|
||||
self.render_pointer(renderer, output, &mut |elem| {
|
||||
pointer.push(elem.into())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor_data = Some(CursorData::compute(&pointer, pointer_pos, scale));
|
||||
}
|
||||
let cursor_data = cursor_data.as_ref().unwrap();
|
||||
|
||||
if cast.dequeue_buffer_and_render(renderer, &elements, cursor_data, size, scale) {
|
||||
cast.last_frame_time = target_presentation_time;
|
||||
}
|
||||
}
|
||||
self.casting.casts = casts;
|
||||
|
||||
for id in casts_to_stop {
|
||||
self.stop_cast(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_windows_for_screen_cast(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
target_presentation_time: Duration,
|
||||
) {
|
||||
let _span = tracy_client::span!("Niri::render_windows_for_screen_cast");
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
|
||||
let mut casts_to_stop = vec![];
|
||||
|
||||
let mut casts = mem::take(&mut self.casting.casts);
|
||||
for cast in &mut casts {
|
||||
if !cast.is_active() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let CastTarget::Window { id } = cast.target else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut windows = self.layout.windows_for_output(output);
|
||||
let Some(mapped) = windows.find(|win| win.id().get() == id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let bbox = mapped
|
||||
.window
|
||||
.bbox_with_popups()
|
||||
.to_physical_precise_up(scale);
|
||||
|
||||
match cast.ensure_size(bbox.size) {
|
||||
Ok(CastSizeChange::Ready) => (),
|
||||
Ok(CastSizeChange::Pending) => continue,
|
||||
Err(err) => {
|
||||
warn!("error updating stream size, stopping screencast: {err:?}");
|
||||
casts_to_stop.push(cast.session_id);
|
||||
}
|
||||
}
|
||||
|
||||
if cast.check_time_and_schedule(output, target_presentation_time) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut elements = Vec::new();
|
||||
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
|
||||
elements.push(CastRenderElement::from(elem))
|
||||
});
|
||||
|
||||
let mut pointer_elements = Vec::new();
|
||||
let mut pointer_location = Point::default();
|
||||
|
||||
if self.pointer_visibility.is_visible() {
|
||||
if let Some((pointer_pos, win_pos)) = self.pointer_pos_for_window_cast(mapped) {
|
||||
// Pointer location must be relative to the screencast buffer.
|
||||
// - win_pos is the position of the main window surface in output-local
|
||||
// coordinates
|
||||
// - bbox.loc moves us relative to the screencast buffer
|
||||
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
|
||||
let output_pos = self.global_space.output_geometry(output).unwrap().loc;
|
||||
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
|
||||
|
||||
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
|
||||
self.render_pointer(renderer, output, &mut |elem| {
|
||||
let elem =
|
||||
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
|
||||
pointer_elements.push(CastRenderElement::from(elem));
|
||||
});
|
||||
}
|
||||
}
|
||||
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
|
||||
|
||||
if cast.dequeue_buffer_and_render(renderer, &elements, &cursor_data, bbox.size, scale) {
|
||||
cast.last_frame_time = target_presentation_time;
|
||||
}
|
||||
}
|
||||
self.casting.casts = casts;
|
||||
|
||||
for id in casts_to_stop {
|
||||
self.stop_cast(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_cast(&mut self, session_id: CastSessionId) {
|
||||
let _span = tracy_client::span!("Niri::stop_cast");
|
||||
let _span = debug_span!("stop_cast", %session_id).entered();
|
||||
|
||||
self.casting
|
||||
.pending_dynamic_casts
|
||||
.retain(|p| p.session_id != session_id);
|
||||
|
||||
for i in (0..self.casting.casts.len()).rev() {
|
||||
let cast = &self.casting.casts[i];
|
||||
if cast.session_id != session_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cast = self.casting.casts.swap_remove(i);
|
||||
if let Err(err) = cast.stream.disconnect() {
|
||||
warn!("error disconnecting stream: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let dbus = &self.dbus.as_ref().unwrap();
|
||||
let server = dbus.conn_screen_cast.as_ref().unwrap().object_server();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id.get());
|
||||
if let Ok(iface) = server.interface::<_, mutter_screen_cast::Session>(path) {
|
||||
let _span = tracy_client::span!("invoking Session::stop");
|
||||
|
||||
async_io::block_on(async move {
|
||||
iface
|
||||
.get()
|
||||
.stop(server.inner(), iface.signal_emitter().clone())
|
||||
.await
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_casts_for_target(&mut self, target: CastTarget) {
|
||||
let _span = tracy_client::span!("Niri::stop_casts_for_target");
|
||||
|
||||
// This is O(N^2) but it shouldn't be a problem I think.
|
||||
let mut saw_dynamic = false;
|
||||
let mut ids = Vec::new();
|
||||
for cast in &self.casting.casts {
|
||||
if cast.target != target {
|
||||
continue;
|
||||
}
|
||||
|
||||
if cast.dynamic_target {
|
||||
saw_dynamic = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
ids.push(cast.session_id);
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
self.stop_cast(id);
|
||||
}
|
||||
|
||||
// We don't stop dynamic casts, instead we switch them to Nothing.
|
||||
if saw_dynamic {
|
||||
self.event_loop
|
||||
.insert_idle(|state| state.set_dynamic_cast_target(CastTarget::Nothing));
|
||||
}
|
||||
}
|
||||
|
||||
fn cast_params_for_window(&self, window_id: u64) -> Option<(Size<i32, Physical>, u32)> {
|
||||
let (_, mapped) = self
|
||||
.layout
|
||||
.windows()
|
||||
.find(|(_, m)| m.id().get() == window_id)?;
|
||||
let output = self.casting.mapped_cast_output.get(&mapped.window)?;
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
let bbox = mapped
|
||||
.window
|
||||
.bbox_with_popups()
|
||||
.to_physical_precise_up(scale);
|
||||
let refresh = output.current_mode().unwrap().refresh as u32;
|
||||
Some((bbox.size, refresh))
|
||||
}
|
||||
}
|
||||
|
||||
fn cast_params_for_output(output: &Output) -> (Size<i32, Physical>, u32) {
|
||||
let mode = output.current_mode().unwrap();
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(mode.size);
|
||||
let refresh = mode.refresh as u32;
|
||||
(size, refresh)
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
CastRenderElement<R> => {
|
||||
Output = OutputRenderElements<R>,
|
||||
Window = WindowCastRenderElements<R>,
|
||||
Pointer = PointerRenderElements<R>,
|
||||
RelocatedPointer = RelocateRenderElement<PointerRenderElements<R>>,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{mem, slice};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
@@ -29,31 +30,46 @@ use pipewire::spa::utils::{
|
||||
};
|
||||
use pipewire::spa::{self};
|
||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamRc, StreamState};
|
||||
use pipewire::sys::{pw_buffer, pw_stream_queue_buffer};
|
||||
use pipewire::sys::{pw_buffer, pw_check_library_version, pw_stream_queue_buffer};
|
||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::{Format, Fourcc};
|
||||
use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::ExportMem;
|
||||
use smithay::output::{Output, OutputModeSource};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Scale, Size, Transform};
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
||||
use crate::niri::{CastTarget, State};
|
||||
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
|
||||
use crate::utils::get_monotonic_time;
|
||||
use crate::render_helpers::{
|
||||
clear_dmabuf, encompassing_geo, render_and_download, render_to_dmabuf,
|
||||
};
|
||||
use crate::screencasting::CastRenderElement;
|
||||
use crate::utils::{get_monotonic_time, CastSessionId, CastStreamId};
|
||||
|
||||
// Give a 0.1 ms allowance for presentation time errors.
|
||||
const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
|
||||
|
||||
const CURSOR_FORMAT: spa_video_format = SPA_VIDEO_FORMAT_BGRA;
|
||||
const CURSOR_BPP: u32 = 4;
|
||||
const CURSOR_WIDTH: u32 = 384;
|
||||
const CURSOR_HEIGHT: u32 = 384;
|
||||
const CURSOR_BITMAP_SIZE: usize = (CURSOR_WIDTH * CURSOR_HEIGHT * CURSOR_BPP) as usize;
|
||||
const CURSOR_META_SIZE: usize =
|
||||
mem::size_of::<spa_meta_cursor>() + mem::size_of::<spa_meta_bitmap>() + CURSOR_BITMAP_SIZE;
|
||||
const BITMAP_META_OFFSET: usize = mem::size_of::<spa_meta_cursor>();
|
||||
const BITMAP_DATA_OFFSET: usize = mem::size_of::<spa_meta_bitmap>();
|
||||
|
||||
pub struct PipeWire {
|
||||
_context: ContextRc,
|
||||
pub core: CoreRc,
|
||||
@@ -63,22 +79,23 @@ pub struct PipeWire {
|
||||
}
|
||||
|
||||
pub enum PwToNiri {
|
||||
StopCast { session_id: usize },
|
||||
Redraw { stream_id: usize },
|
||||
StopCast { session_id: CastSessionId },
|
||||
Redraw { stream_id: CastStreamId },
|
||||
FatalError,
|
||||
}
|
||||
|
||||
pub struct Cast {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
pub session_id: usize,
|
||||
pub stream_id: usize,
|
||||
pub stream: StreamRc,
|
||||
pub session_id: CastSessionId,
|
||||
pub stream_id: CastStreamId,
|
||||
// Listener is dropped before Stream to prevent a use-after-free.
|
||||
_listener: StreamListener<()>,
|
||||
pub stream: StreamRc,
|
||||
pub target: CastTarget,
|
||||
pub dynamic_target: bool,
|
||||
formats: FormatSet,
|
||||
offer_alpha: bool,
|
||||
pub cursor_mode: CursorMode,
|
||||
cursor_mode: CursorMode,
|
||||
pub last_frame_time: Duration,
|
||||
scheduled_redraw: Option<RegistrationToken>,
|
||||
// Incremented once per successful frame, stored in buffer meta.
|
||||
@@ -123,6 +140,8 @@ enum CastState {
|
||||
plane_count: i32,
|
||||
// Lazily-initialized to keep the initialization to a single place.
|
||||
damage_tracker: Option<OutputDamageTracker>,
|
||||
cursor_damage_tracker: Option<OutputDamageTracker>,
|
||||
last_cursor_location: Option<Point<i32, Physical>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,6 +151,49 @@ pub enum CastSizeChange {
|
||||
Pending,
|
||||
}
|
||||
|
||||
/// Data for drawing a cursor either as metadata or embedded.
|
||||
///
|
||||
/// We have weird borrowed references here in order to support both metadata and embedded cases.
|
||||
/// The cursor damage tracker needs a slice of impl Element at (0, 0), so we pass it `relocated`
|
||||
/// (luckily, &impl Element also impls Element). Then, if we need to embed the cursor, we chain the
|
||||
/// elements to the main video buffer elements, so we need the same type. We use `original` for
|
||||
/// this; `E` is expected to match the type of the main video buffer elements.
|
||||
#[derive(Debug)]
|
||||
pub struct CursorData<'a, E> {
|
||||
/// Cursor elements at their original location.
|
||||
original: &'a [E],
|
||||
/// Cursor elements relocated to (0, 0).
|
||||
relocated: Vec<RelocateRenderElement<&'a E>>,
|
||||
/// Location of the cursor's hotspot in the video buffer.
|
||||
location: Point<i32, Physical>,
|
||||
/// Location of the cursor's hotspot on the cursor bitmap.
|
||||
hotspot: Point<i32, Physical>,
|
||||
/// Size of the elements' encompassing geo.
|
||||
size: Size<i32, Physical>,
|
||||
/// Scale the elements should be rendered at.
|
||||
scale: Scale<f64>,
|
||||
}
|
||||
|
||||
impl<'a, E: Element> CursorData<'a, E> {
|
||||
pub fn compute(elements: &'a [E], location: Point<f64, Logical>, scale: Scale<f64>) -> Self {
|
||||
let location = location.to_physical_precise_round(scale);
|
||||
|
||||
let geo = encompassing_geo(scale, elements.iter());
|
||||
let relocated = Vec::from_iter(elements.iter().map(|elem| {
|
||||
RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative)
|
||||
}));
|
||||
|
||||
Self {
|
||||
original: elements,
|
||||
relocated,
|
||||
location,
|
||||
hotspot: location - geo.loc,
|
||||
size: geo.size,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! make_params {
|
||||
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||
let mut b1 = Vec::new();
|
||||
@@ -207,14 +269,13 @@ impl PipeWire {
|
||||
&self,
|
||||
gbm: GbmDevice<DrmDeviceFd>,
|
||||
formats: FormatSet,
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
session_id: CastSessionId,
|
||||
stream_id: CastStreamId,
|
||||
target: CastTarget,
|
||||
dynamic_target: bool,
|
||||
size: Size<i32, Physical>,
|
||||
refresh: u32,
|
||||
alpha: bool,
|
||||
cursor_mode: CursorMode,
|
||||
mut cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
) -> anyhow::Result<Cast> {
|
||||
let _span = tracy_client::span!("PipeWire::start_cast");
|
||||
@@ -222,13 +283,13 @@ impl PipeWire {
|
||||
let to_niri_ = self.to_niri.clone();
|
||||
let stop_cast = move || {
|
||||
if let Err(err) = to_niri_.send(PwToNiri::StopCast { session_id }) {
|
||||
warn!(session_id, "error sending StopCast to niri: {err:?}");
|
||||
warn!(%session_id, "error sending StopCast to niri: {err:?}");
|
||||
}
|
||||
};
|
||||
let to_niri_ = self.to_niri.clone();
|
||||
let redraw = move || {
|
||||
if let Err(err) = to_niri_.send(PwToNiri::Redraw { stream_id }) {
|
||||
warn!(stream_id, "error sending Redraw to niri: {err:?}");
|
||||
warn!(%stream_id, "error sending Redraw to niri: {err:?}");
|
||||
}
|
||||
};
|
||||
let redraw_ = redraw.clone();
|
||||
@@ -240,6 +301,14 @@ impl PipeWire {
|
||||
)
|
||||
.context("error creating Stream")?;
|
||||
|
||||
if cursor_mode == CursorMode::Metadata && !pw_version_supports_cursor_metadata() {
|
||||
debug!(
|
||||
"metadata cursor mode requested, but PipeWire is too old (need >= 1.4.8); \
|
||||
switching to embedded cursor"
|
||||
);
|
||||
cursor_mode = CursorMode::Embedded;
|
||||
}
|
||||
|
||||
let pending_size = Size::from((size.w as u32, size.h as u32));
|
||||
|
||||
// Like in good old wayland-rs times...
|
||||
@@ -259,7 +328,8 @@ impl PipeWire {
|
||||
let inner = inner.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |stream, (), old, new| {
|
||||
debug!(stream_id, "pw stream: state changed: {old:?} -> {new:?}");
|
||||
let _span = debug_span!("state_changed", %stream_id).entered();
|
||||
debug!("{old:?} -> {new:?}");
|
||||
let mut inner = inner.borrow_mut();
|
||||
|
||||
match new {
|
||||
@@ -267,7 +337,7 @@ impl PipeWire {
|
||||
if inner.node_id.is_none() {
|
||||
let id = stream.node_id();
|
||||
inner.node_id = Some(id);
|
||||
debug!(stream_id, "pw stream: sending signal with {id}");
|
||||
debug!("sending signal with {id}");
|
||||
|
||||
let _span = tracy_client::span!("sending PipeWireStreamAdded");
|
||||
async_io::block_on(async {
|
||||
@@ -278,10 +348,7 @@ impl PipeWire {
|
||||
.await;
|
||||
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
stream_id,
|
||||
"error sending PipeWireStreamAdded: {err:?}"
|
||||
);
|
||||
warn!("error sending PipeWireStreamAdded: {err:?}");
|
||||
stop_cast();
|
||||
}
|
||||
});
|
||||
@@ -311,7 +378,7 @@ impl PipeWire {
|
||||
let formats = formats.clone();
|
||||
move |stream, (), id, pod| {
|
||||
let id = ParamType::from_raw(id);
|
||||
trace!(stream_id, ?id, "pw stream: param_changed");
|
||||
trace!(%stream_id, ?id, "param_changed");
|
||||
let mut inner = inner.borrow_mut();
|
||||
let inner = &mut *inner;
|
||||
|
||||
@@ -319,12 +386,14 @@ impl PipeWire {
|
||||
return;
|
||||
}
|
||||
|
||||
let _span = debug_span!("param_changed", %stream_id).entered();
|
||||
|
||||
let Some(pod) = pod else { return };
|
||||
|
||||
let (m_type, m_subtype) = match parse_format(pod) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(stream_id, "pw stream: error parsing format: {err:?}");
|
||||
warn!("error parsing format: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -335,19 +404,19 @@ impl PipeWire {
|
||||
|
||||
let mut format = VideoInfoRaw::new();
|
||||
format.parse(pod).unwrap();
|
||||
debug!(stream_id, "pw stream: got format = {format:?}");
|
||||
debug!("got format = {format:?}");
|
||||
|
||||
let format_size = Size::from((format.size().width, format.size().height));
|
||||
|
||||
let state = &mut inner.state;
|
||||
if format_size != state.expected_format_size() {
|
||||
if !matches!(&*state, CastState::ResizePending { .. }) {
|
||||
warn!(stream_id, "pw stream: wrong size, but we're not resizing");
|
||||
warn!("wrong size, but we're not resizing");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(stream_id, "pw stream: wrong size, waiting");
|
||||
debug!("wrong size, waiting");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -368,25 +437,25 @@ impl PipeWire {
|
||||
let Some(prop_modifier) =
|
||||
object.find_prop(spa::utils::Id(FormatProperties::VideoModifier.0))
|
||||
else {
|
||||
warn!(stream_id, "pw stream: modifier prop missing");
|
||||
warn!("modifier prop missing");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
|
||||
if prop_modifier.flags().contains(PodPropFlags::DONT_FIXATE) {
|
||||
debug!(stream_id, "pw stream: fixating the modifier");
|
||||
debug!("fixating the modifier");
|
||||
|
||||
let pod_modifier = prop_modifier.value();
|
||||
let Ok((_, modifiers)) = PodDeserializer::deserialize_from::<Choice<i64>>(
|
||||
pod_modifier.as_bytes(),
|
||||
) else {
|
||||
warn!(stream_id, "pw stream: wrong modifier property type");
|
||||
warn!("wrong modifier property type");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
|
||||
let ChoiceEnum::Enum { alternatives, .. } = modifiers.1 else {
|
||||
warn!(stream_id, "pw stream: wrong modifier choice type");
|
||||
warn!("wrong modifier choice type");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
@@ -399,18 +468,14 @@ impl PipeWire {
|
||||
) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
stream_id,
|
||||
"pw stream: couldn't find preferred modifier: {err:?}"
|
||||
);
|
||||
warn!("couldn't find preferred modifier: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
stream_id,
|
||||
"pw stream: allocation successful \
|
||||
"allocation successful \
|
||||
(modifier={modifier:?}, plane_count={plane_count}), \
|
||||
moving to confirmation pending"
|
||||
);
|
||||
@@ -447,7 +512,7 @@ impl PipeWire {
|
||||
let mut params = [pod1, make_pod(&mut b2, o2)];
|
||||
|
||||
if let Err(err) = stream.update_params(&mut params) {
|
||||
warn!(stream_id, "error updating stream params: {err:?}");
|
||||
warn!("error updating stream params: {err:?}");
|
||||
stop_cast();
|
||||
}
|
||||
|
||||
@@ -476,14 +541,19 @@ impl PipeWire {
|
||||
let modifier = *modifier;
|
||||
let plane_count = *plane_count;
|
||||
|
||||
let damage_tracker =
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut *state {
|
||||
damage_tracker.take()
|
||||
let (damage_tracker, cursor_damage_tracker) =
|
||||
if let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
..
|
||||
} = &mut *state
|
||||
{
|
||||
(damage_tracker.take(), cursor_damage_tracker.take())
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
debug!(stream_id, "pw stream: moving to ready state");
|
||||
debug!("moving to ready state");
|
||||
|
||||
*state = CastState::Ready {
|
||||
size,
|
||||
@@ -491,6 +561,8 @@ impl PipeWire {
|
||||
modifier,
|
||||
plane_count,
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
last_cursor_location: None,
|
||||
};
|
||||
|
||||
plane_count
|
||||
@@ -506,15 +578,14 @@ impl PipeWire {
|
||||
) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(stream_id, "pw stream: test allocation failed: {err:?}");
|
||||
warn!("test allocation failed: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
stream_id,
|
||||
"pw stream: allocation successful \
|
||||
"allocation successful \
|
||||
(modifier={modifier:?}, plane_count={plane_count}), \
|
||||
moving to ready"
|
||||
);
|
||||
@@ -525,6 +596,8 @@ impl PipeWire {
|
||||
modifier,
|
||||
plane_count: plane_count as i32,
|
||||
damage_tracker: None,
|
||||
cursor_damage_tracker: None,
|
||||
last_cursor_location: None,
|
||||
};
|
||||
|
||||
plane_count as i32
|
||||
@@ -543,7 +616,7 @@ impl PipeWire {
|
||||
pod::Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 16,
|
||||
default: 8,
|
||||
min: 2,
|
||||
max: 16
|
||||
}
|
||||
@@ -562,8 +635,6 @@ impl PipeWire {
|
||||
),
|
||||
);
|
||||
|
||||
// FIXME: Hidden / embedded / metadata cursor
|
||||
|
||||
let o2 = pod::object!(
|
||||
SpaTypes::ObjectParamMeta,
|
||||
ParamType::Meta,
|
||||
@@ -578,10 +649,27 @@ impl PipeWire {
|
||||
);
|
||||
let mut b1 = vec![];
|
||||
let mut b2 = vec![];
|
||||
let mut params = [make_pod(&mut b1, o1), make_pod(&mut b2, o2)];
|
||||
let mut params = vec![make_pod(&mut b1, o1), make_pod(&mut b2, o2)];
|
||||
|
||||
let mut b_cursor = vec![];
|
||||
if cursor_mode == CursorMode::Metadata {
|
||||
let o_cursor = pod::object!(
|
||||
SpaTypes::ObjectParamMeta,
|
||||
ParamType::Meta,
|
||||
Property::new(
|
||||
SPA_PARAM_META_type,
|
||||
pod::Value::Id(spa::utils::Id(SPA_META_Cursor))
|
||||
),
|
||||
Property::new(
|
||||
SPA_PARAM_META_size,
|
||||
pod::Value::Int(CURSOR_META_SIZE as i32)
|
||||
),
|
||||
);
|
||||
params.push(make_pod(&mut b_cursor, o_cursor));
|
||||
}
|
||||
|
||||
if let Err(err) = stream.update_params(&mut params) {
|
||||
warn!(stream_id, "error updating stream params: {err:?}");
|
||||
warn!("error updating stream params: {err:?}");
|
||||
stop_cast();
|
||||
}
|
||||
}
|
||||
@@ -590,6 +678,7 @@ impl PipeWire {
|
||||
let inner = inner.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
move |stream, (), buffer| {
|
||||
let _span = debug_span!("add_buffer", %stream_id).entered();
|
||||
let mut inner = inner.borrow_mut();
|
||||
|
||||
let (size, alpha, modifier) = if let CastState::Ready {
|
||||
@@ -601,15 +690,11 @@ impl PipeWire {
|
||||
{
|
||||
(*size, *alpha, *modifier)
|
||||
} else {
|
||||
trace!(stream_id, "pw stream: add buffer, but not ready yet");
|
||||
trace!("add_buffer, but not ready yet");
|
||||
return;
|
||||
};
|
||||
|
||||
trace!(
|
||||
stream_id,
|
||||
"pw stream: add_buffer, size={size:?}, alpha={alpha}, \
|
||||
modifier={modifier:?}"
|
||||
);
|
||||
trace!("size={size:?}, alpha={alpha}, modifier={modifier:?}");
|
||||
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
@@ -623,7 +708,7 @@ impl PipeWire {
|
||||
let dmabuf = match allocate_dmabuf(&gbm, size, fourcc, modifier) {
|
||||
Ok(dmabuf) => dmabuf,
|
||||
Err(err) => {
|
||||
warn!(stream_id, "error allocating dmabuf: {err:?}");
|
||||
warn!("error allocating dmabuf: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
@@ -654,7 +739,6 @@ impl PipeWire {
|
||||
(*chunk).offset = offset;
|
||||
|
||||
trace!(
|
||||
stream_id,
|
||||
"pw buffer plane: fd={}, stride={stride}, offset={offset}",
|
||||
(*spa_data).fd
|
||||
);
|
||||
@@ -674,7 +758,7 @@ impl PipeWire {
|
||||
.remove_buffer({
|
||||
let inner = inner.clone();
|
||||
move |_stream, (), buffer| {
|
||||
trace!(stream_id, "pw stream: remove_buffer");
|
||||
trace!(%stream_id, "remove_buffer");
|
||||
let mut inner = inner.borrow_mut();
|
||||
|
||||
inner
|
||||
@@ -695,7 +779,7 @@ impl PipeWire {
|
||||
.unwrap();
|
||||
|
||||
trace!(
|
||||
stream_id,
|
||||
%stream_id,
|
||||
"starting pw stream with size={pending_size:?}, refresh={refresh:?}"
|
||||
);
|
||||
|
||||
@@ -717,7 +801,7 @@ impl PipeWire {
|
||||
stream,
|
||||
_listener: listener,
|
||||
target,
|
||||
dynamic_target,
|
||||
dynamic_target: false,
|
||||
formats,
|
||||
offer_alpha: alpha,
|
||||
cursor_mode,
|
||||
@@ -735,6 +819,10 @@ impl Cast {
|
||||
self.inner.borrow().is_active
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> Option<u32> {
|
||||
self.inner.borrow().node_id
|
||||
}
|
||||
|
||||
pub fn ensure_size(&self, size: Size<i32, Physical>) -> anyhow::Result<CastSizeChange> {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
@@ -947,7 +1035,7 @@ impl Cast {
|
||||
let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
|
||||
self.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
for cast in &mut state.niri.casts {
|
||||
for cast in &mut state.niri.casting.casts {
|
||||
if cast.stream_id == stream_id {
|
||||
cast.queue_completed_buffers();
|
||||
}
|
||||
@@ -960,21 +1048,36 @@ impl Cast {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dequeue_buffer_and_render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
elements: &[CastRenderElement<GlesRenderer>],
|
||||
cursor_data: &CursorData<CastRenderElement<GlesRenderer>>,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
) -> bool {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
let CastState::Ready { damage_tracker, .. } = &mut inner.state else {
|
||||
let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
last_cursor_location,
|
||||
..
|
||||
} = &mut inner.state
|
||||
else {
|
||||
error!("cast must be in Ready state to render");
|
||||
return false;
|
||||
};
|
||||
let damage_tracker = damage_tracker
|
||||
.get_or_insert_with(|| OutputDamageTracker::new(size, scale, Transform::Normal));
|
||||
let cursor_damage_tracker = cursor_damage_tracker.get_or_insert_with(|| {
|
||||
OutputDamageTracker::new(
|
||||
Size::from((CURSOR_WIDTH as _, CURSOR_HEIGHT as _)),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
)
|
||||
});
|
||||
|
||||
// Size change will drop the damage tracker, but scale change won't, so check it here.
|
||||
let OutputModeSource::Static { scale: t_scale, .. } = damage_tracker.mode() else {
|
||||
@@ -982,13 +1085,31 @@ impl Cast {
|
||||
};
|
||||
if *t_scale != scale {
|
||||
*damage_tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
|
||||
*cursor_damage_tracker = OutputDamageTracker::new(
|
||||
Size::from((CURSOR_WIDTH as _, CURSOR_HEIGHT as _)),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
let (damage, _states) = damage_tracker.damage_output(1, elements).unwrap();
|
||||
if damage.is_none() {
|
||||
|
||||
let mut has_cursor_update = false;
|
||||
let mut redraw_cursor = false;
|
||||
if self.cursor_mode != CursorMode::Hidden {
|
||||
let (damage, _states) = cursor_damage_tracker
|
||||
.damage_output(1, &cursor_data.relocated)
|
||||
.unwrap();
|
||||
redraw_cursor = damage.is_some();
|
||||
has_cursor_update =
|
||||
redraw_cursor || *last_cursor_location != Some(cursor_data.location);
|
||||
}
|
||||
|
||||
if damage.is_none() && !has_cursor_update {
|
||||
trace!("no damage, skipping frame");
|
||||
return false;
|
||||
}
|
||||
*last_cursor_location = Some(cursor_data.location);
|
||||
drop(inner);
|
||||
|
||||
let Some(pw_buffer) = self.dequeue_available_buffer() else {
|
||||
@@ -1000,6 +1121,19 @@ impl Cast {
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
|
||||
let mut pointer_elements = None;
|
||||
if self.cursor_mode == CursorMode::Metadata {
|
||||
add_cursor_metadata(renderer, spa_buffer, cursor_data, redraw_cursor);
|
||||
} else if self.cursor_mode != CursorMode::Hidden {
|
||||
// Embed the cursor into the main render.
|
||||
pointer_elements = Some(cursor_data.original.iter());
|
||||
}
|
||||
let pointer_elements = pointer_elements.into_iter().flatten();
|
||||
let elements = pointer_elements.chain(elements);
|
||||
|
||||
// FIXME: would be good to skip rendering the full frame if only the pointer changed.
|
||||
// Unfortunately, I think the OBS PipeWire code needs to be updated first to cleanly
|
||||
// allow for that codepath.
|
||||
let fd = (*(*spa_buffer).datas).fd;
|
||||
let dmabuf = self.inner.borrow().dmabufs[&fd].clone();
|
||||
|
||||
@@ -1009,7 +1143,7 @@ impl Cast {
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
elements.iter().rev(),
|
||||
elements.rev(),
|
||||
) {
|
||||
Ok(sync_point) => {
|
||||
mark_buffer_as_good(pw_buffer, &mut self.sequence_counter);
|
||||
@@ -1030,8 +1164,14 @@ impl Cast {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
// Clear out the damage tracker if we're in Ready state.
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut inner.state {
|
||||
if let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
..
|
||||
} = &mut inner.state
|
||||
{
|
||||
*damage_tracker = None;
|
||||
*cursor_damage_tracker = None;
|
||||
};
|
||||
drop(inner);
|
||||
|
||||
@@ -1044,6 +1184,10 @@ impl Cast {
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
|
||||
if self.cursor_mode == CursorMode::Metadata {
|
||||
add_invisible_cursor(spa_buffer);
|
||||
}
|
||||
|
||||
let fd = (*(*spa_buffer).datas).fd;
|
||||
let dmabuf = self.inner.borrow().dmabufs[&fd].clone();
|
||||
|
||||
@@ -1082,6 +1226,12 @@ impl CastState {
|
||||
}
|
||||
}
|
||||
|
||||
fn pw_version_supports_cursor_metadata() -> bool {
|
||||
// This PipeWire version fixed a critical memory issue with cursor metadata:
|
||||
// https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2538
|
||||
unsafe { pw_check_library_version(1, 4, 8) }
|
||||
}
|
||||
|
||||
fn make_video_params(
|
||||
formats: &FormatSet,
|
||||
size: Size<u32, Physical>,
|
||||
@@ -1278,3 +1428,146 @@ unsafe fn find_meta_header(buffer: *mut spa_buffer) -> Option<NonNull<spa_meta_h
|
||||
let p = spa_buffer_find_meta_data(buffer, SPA_META_Header, size_of::<spa_meta_header>()).cast();
|
||||
NonNull::new(p)
|
||||
}
|
||||
|
||||
unsafe fn add_invisible_cursor(spa_buffer: *mut spa_buffer) {
|
||||
unsafe {
|
||||
let cursor_meta_ptr: *mut spa_meta_cursor = spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
SPA_META_Cursor,
|
||||
mem::size_of::<spa_meta_cursor>(),
|
||||
)
|
||||
.cast();
|
||||
let Some(cursor_meta) = cursor_meta_ptr.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The cursor is present but invisible.
|
||||
cursor_meta.id = 1;
|
||||
cursor_meta.position.x = 0;
|
||||
cursor_meta.position.y = 0;
|
||||
cursor_meta.hotspot.x = 0;
|
||||
cursor_meta.hotspot.y = 0;
|
||||
cursor_meta.bitmap_offset = BITMAP_META_OFFSET as _;
|
||||
|
||||
let bitmap_meta_ptr = cursor_meta_ptr
|
||||
.byte_add(BITMAP_META_OFFSET)
|
||||
.cast::<spa_meta_bitmap>();
|
||||
let bitmap_meta = &mut *bitmap_meta_ptr;
|
||||
|
||||
// HACK: PipeWire docs say offset = 0 means invisible.
|
||||
//
|
||||
// Unfortunately, OBS doesn't actually check that, instead it checks that size isn't zero:
|
||||
// https://github.com/obsproject/obs-studio/blob/f4aaa5f0417c5ec40a3799551e125129fce1e007/plugins/linux-pipewire/pipewire.c#L900
|
||||
//
|
||||
// Unfortunately, libwebrtc, on top of ignoring offset, also treats size = 0 as "preserve
|
||||
// previous cursor":
|
||||
// https://webrtc.googlesource.com/src/+/97b46e12582606a238d4f0c8524365cf5bdcb411/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc#765
|
||||
//
|
||||
// So, send a 1x1 transparent pixel instead...
|
||||
bitmap_meta.offset = BITMAP_DATA_OFFSET as _;
|
||||
bitmap_meta.size.width = 1;
|
||||
bitmap_meta.size.height = 1;
|
||||
bitmap_meta.stride = CURSOR_BPP as i32;
|
||||
bitmap_meta.format = CURSOR_FORMAT;
|
||||
|
||||
let bitmap_data = bitmap_meta_ptr.cast::<u8>().add(BITMAP_DATA_OFFSET);
|
||||
let bitmap_slice = slice::from_raw_parts_mut(bitmap_data, CURSOR_BITMAP_SIZE);
|
||||
bitmap_slice[..4].copy_from_slice(&[0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn add_cursor_metadata(
|
||||
renderer: &mut GlesRenderer,
|
||||
spa_buffer: *mut spa_buffer,
|
||||
cursor_data: &CursorData<impl RenderElement<GlesRenderer>>,
|
||||
redraw: bool,
|
||||
) {
|
||||
unsafe {
|
||||
let cursor_meta_ptr: *mut spa_meta_cursor = spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
SPA_META_Cursor,
|
||||
mem::size_of::<spa_meta_cursor>(),
|
||||
)
|
||||
.cast();
|
||||
let Some(cursor_meta) = cursor_meta_ptr.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cursor_meta.id = 1;
|
||||
cursor_meta.position.x = cursor_data.location.x;
|
||||
cursor_meta.position.y = cursor_data.location.y;
|
||||
cursor_meta.hotspot.x = cursor_data.hotspot.x;
|
||||
cursor_meta.hotspot.y = cursor_data.hotspot.y;
|
||||
|
||||
if !redraw {
|
||||
trace!("cursor not damaged, skipping rerendering");
|
||||
cursor_meta.bitmap_offset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
cursor_meta.bitmap_offset = BITMAP_META_OFFSET as _;
|
||||
|
||||
let bitmap_meta_ptr = cursor_meta_ptr
|
||||
.byte_add(BITMAP_META_OFFSET)
|
||||
.cast::<spa_meta_bitmap>();
|
||||
let bitmap_meta = &mut *bitmap_meta_ptr;
|
||||
|
||||
// Start with a 1x1 transparent pixel; see comment in add_invisible_cursor().
|
||||
bitmap_meta.offset = BITMAP_DATA_OFFSET as _;
|
||||
bitmap_meta.size.width = 1;
|
||||
bitmap_meta.size.height = 1;
|
||||
bitmap_meta.stride = CURSOR_BPP as i32;
|
||||
bitmap_meta.format = CURSOR_FORMAT;
|
||||
|
||||
let bitmap_data = bitmap_meta_ptr.cast::<u8>().add(BITMAP_DATA_OFFSET);
|
||||
let bitmap_slice = slice::from_raw_parts_mut(bitmap_data, CURSOR_BITMAP_SIZE);
|
||||
bitmap_slice[..4].copy_from_slice(&[0, 0, 0, 0]);
|
||||
|
||||
let size = Size::new(
|
||||
min(cursor_data.size.w, CURSOR_WIDTH as i32),
|
||||
min(cursor_data.size.h, CURSOR_HEIGHT as i32),
|
||||
);
|
||||
if size.w == 0 || size.h == 0 {
|
||||
trace!("cursor is invisible, skipping rendering");
|
||||
return;
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!("add_cursor_metadata render cursor");
|
||||
|
||||
// FIXME: use a reliable buffer whenever we're rendering the cursor.
|
||||
//
|
||||
// PipeWire buffers are not normally guaranteed to reach the destination, so our buffer
|
||||
// with the rendered cursor bitmap may not reach the consumer.
|
||||
//
|
||||
// Reliable buffers should be available starting from 1.6.0:
|
||||
// https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/4885
|
||||
let mapping = match render_and_download(
|
||||
renderer,
|
||||
size,
|
||||
cursor_data.scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Argb8888,
|
||||
cursor_data.relocated.iter().rev(),
|
||||
) {
|
||||
Ok(mapping) => mapping,
|
||||
Err(err) => {
|
||||
warn!("error rendering cursor: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pixels = match renderer.map_texture(&mapping) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(err) => {
|
||||
warn!("error mapping cursor texture: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
bitmap_slice[..pixels.len()].copy_from_slice(pixels);
|
||||
|
||||
// Fill the metadata now that everything succeeded.
|
||||
bitmap_meta.size.width = size.w as _;
|
||||
bitmap_meta.size.height = size.h as _;
|
||||
bitmap_meta.stride = size.w * CURSOR_BPP as i32;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "config:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n}"
|
||||
description: "config:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "set parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
description: "set parent: A1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "set parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
description: "set parent: A2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "set parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
description: "set parent: B1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "set parent: B2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
description: "set parent: B2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A1\nset parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
description: "want fullscreen: A1\nset parent: A1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A1\nset parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
description: "want fullscreen: A1\nset parent: A2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A1\nset parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
description: "want fullscreen: A1\nset parent: B1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A1\nset parent: B2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
description: "want fullscreen: A1\nset parent: B2\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}"
|
||||
description: "want fullscreen: A1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/tests/window_opening.rs
|
||||
description: "want fullscreen: A2\nset parent: A1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
description: "want fullscreen: A2\nset parent: A1\nconfig:\noutput \"headless-2\" {\n layout {\n border {\n on\n }\n }\n}\n\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n\n layout {\n border {\n width 10\n }\n\n default-column-width {\n fixed 500\n }\n }\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
|
||||
expression: snapshot
|
||||
---
|
||||
final monitor: headless-1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user