Compare commits

...

205 Commits

Author SHA1 Message Date
dependabot[bot] af8fb49dd0 build(deps): bump pangocairo from 0.21.5 to 0.22.0
Bumps [pangocairo](https://github.com/gtk-rs/gtk-rs-core) from 0.21.5 to 0.22.0.
- [Release notes](https://github.com/gtk-rs/gtk-rs-core/releases)
- [Changelog](https://github.com/gtk-rs/gtk-rs-core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gtk-rs/gtk-rs-core/compare/0.21.5...0.22.0)

---
updated-dependencies:
- dependency-name: pangocairo
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-25 06:28:57 +00:00
Ivan Molodetskikh 8fd9fb73f2 Bump Since on the wiki 2026-04-25 09:16:29 +03:00
Ivan Molodetskikh 8d83fbae67 CI: Update Fedora to 42
41 is EOL
2026-04-25 09:12:26 +03:00
Ivan Molodetskikh 414729dce5 Bump version to 26.04 2026-04-25 09:12:26 +03:00
Ivan Molodetskikh dbe79b7873 CI: Fix next release check in release workflow 2026-04-25 09:12:16 +03:00
Ivan Molodetskikh 9438f59e2b Bump Smithay (last fix was merged) 2026-04-24 18:45:29 +03:00
Ivan Molodetskikh 719255ac35 Bump Smithay to fix GTK 4.23 text-input panic 2026-04-24 18:00:34 +03:00
Ivan Molodetskikh 8a51935224 wiki: Update blur example from foot to alacritty 2026-04-24 15:38:50 +03:00
mgabor3141 8d583fe854 Preserve num lock state when loading custom keymap 2026-04-23 23:52:47 -07:00
Ivan Molodetskikh 74d2b18603 wiki: Mention Mod+M default bind 2026-04-22 13:24:01 +03:00
Ivan Molodetskikh fad02316f1 wiki: Update binds list on Getting Started 2026-04-22 13:22:57 +03:00
Ivan Molodetskikh 47385c2ecd wiki: Add missing Since next release 2026-04-22 13:18:39 +03:00
Uzlkav e430d3ab2b wiki: explicitly mention possible include methods 2026-04-22 13:18:14 +03:00
Ivan Molodetskikh e472b5b0f1 wiki: Fix typos 2026-04-21 22:35:13 +03:00
Ivan Molodetskikh efb169416d wiki: Mention these are the default blur values 2026-04-21 22:35:06 +03:00
Ivan Molodetskikh 3a3a97ec2a Remove wrong return before dlclose() 2026-04-21 17:58:59 +03:00
Ivan Molodetskikh e9c182a13c Make use of new DndGrabHandler::cancelled() 2026-04-20 20:42:21 +03:00
Ivan Molodetskikh cfe059c303 Fix clipped surface + y_invert buffer 2026-04-20 20:38:26 +03:00
Ivan Molodetskikh bd7c748a4f Upgrade Smithay (old laptop screenshot fix) 2026-04-20 20:37:33 +03:00
Ivan Molodetskikh 68bb942d21 Add test for set_fullscreen panic on removed wl_output 2026-04-19 14:35:36 +03:00
jan Lemata 04c422e43f tty: Re-evaluate ignored nodes on udev add events and session resume (#3651)
* tty: Re-evaluate ignored nodes on udev add events

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

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

* fixes

---------

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

* fixes

---------

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

* reword

---------

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

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

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

* fixes

---------

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

document why we have duplicate destruction logic in foreign toplevel

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

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

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

https://github.com/AccessKit/accesskit-c/issues/76
2026-04-16 09:54:48 +03:00
Ivan Molodetskikh 71d7fa9a61 effect_buffer: Change debug! to trace! 2026-04-15 21:35:28 +03:00
Ivan Molodetskikh 707f08559c postprocess: Replace IGN with more white-noise-like noise
IGN gives visible patterns when zooming in or increasing noise amount.
2026-04-15 21:28:22 +03:00
Ivan Molodetskikh 4d21489101 Add geometry-corner-radius and background-effect popup rules 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 5a24aae560 Add popups { opacity } window and layer rule 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 9170161a0a Implement ext-background-effect for popups 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 66d66d6030 Implement non-xray background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 19866f8b0b Refactor screencopy/screencast to render via the damage tracker, fix cursorless screencopy with damage
The damage tracker stores framebuffer effect cache, so we want anything
that renders repeatedly to render through a reused damage tracker. This
way, the cache persists and is reused across renders.

This will be important for the non-xray background effects.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 4bc3ede4b7 blur: Update to use GlesRenderer in a subframe
Will be necessary for non-xray.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 250aa1f3cb Upgrade Smithay (framebuffer effect, subframes) 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 0117d6953d Document background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 931123f38c Implement ext-background-effect protocol 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 73c0ce75d8 Implement blur background effect 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 1b1715fe9b Implement noise and saturation background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh fee8719299 Implement xray background effect 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 7f9c7d1415 Get state in one place instead of three 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh b81cb13c2c Simplify popup position computation 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 45582ad095 render_helpers: Extract common functions 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh b3f5255bb9 layout: Narrow error! condition in update_render_elements()
As far as I can tell, the only place where we can hit this currently is
do-screen-transition when no outputs are connected. A check could be
trivially added there, but I don't think it's worth enforcing it with an
error! -- it's just an optimization (avoiding running unnecessary code)
while being rare and difficult to find if it does get logged.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 8c169b1a14 offscreen: Guard against zero size (empty elements) 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 525b33777b offscreen: Add error message 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh dec0e3bf5a layout: Improve view_rect computation for the interactively moved window 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 6bcaaf9d21 Add layer matcher to layer-rule 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh f022b3c504 damage: Remove set_size()
Changes in geometry already cause a repaint.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh ab10a260fa Rename Niri::render_inner() => render() 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 0eddd16b8a Rename Niri::render() => render_to_vec() 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh d020d986ed shaders: Optimize rounding_alpha() a little
Simplified clamping exploiting half_px + half_px = 1.0 / niri_scale.

Makes the resize shader fit on a Eee PC ALU.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 5abeb923de shaders: Extract niri_rounding_alpha() into a file
To have it in one place.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh dd1f28998f Bundle renderer and target into a RenderCtx 2026-04-15 14:35:14 +03:00
Semper_ 874e7fd70e Fix typo in switch-preset-window-height-back 2026-04-14 10:02:37 +03:00
Emir Karamehmetoglu 599db847f8 Docs: document reverse switching functionality introduced in #1670 (#3782)
* document the reverse switching functionality introduced in #1670

Toggling in reverse through preset widths & heights was added in #1670.

However, it's really difficult to find in the docs. The sole exception is a comment in the default config.kdl, but that only documents one of the new settings (width).

I had to open up `bindings.rs` on github source code to even find the right setting for the other. This information should be available in the docs or config somewhere.

## Alternatives Considered

I considered documenting the preset-toggling functionality, including reverse toggling, in `bindings` wiki, but then the original (non-reversed) toggling would be documented in multiple places. More importantly, bindings seems to be a guide on how to set bindings, not what actions are available for use with bindings.

* Update docs/wiki/Configuration:-Layout.md

* Update docs/wiki/Configuration:-Layout.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-07 11:25:55 +00:00
HigherOrderLogic d1a0380eed ci: update actions/checkout to v6 2026-04-05 14:43:37 +03:00
Jakob Hellermann 8f48f56fe1 ui/hotkey_overlay: prettify diaeresis keysyms 2026-03-27 16:00:18 +03:00
LuckShiba b07bde3ee8 nix: fix NIRI_BUILD_COMMIT when tree is dirty/shortRev is not available 2026-03-10 16:09:44 +08:00
Ivan Molodetskikh bf142e0b48 Upgrade csscolorparser to v0.8.3 (fixed MSRV) 2026-03-09 12:26:03 +03:00
Ivan Molodetskikh 8f75d171b6 CI: Switch tests to Ubuntu 26.04 2026-03-07 11:57:07 +03:00
Ivan Molodetskikh cbf4631461 Downgrade csscolorparser to fix MSRV 2026-03-07 11:36:33 +03:00
Ivan Molodetskikh a217ad6424 tty: Log on DrmScanEvent::Changed 2026-03-07 08:54:39 +03:00
Ivan Molodetskikh f4dc10e0b4 Upgrade Smithay (virtual_keyboard revert, clipboard destroy cleanup, xdg-shell v7)
The virtual keyboard is reverted to the same state as it was in v25.08, i.e.
cannot trigger compositor binds. A bigger Smithay refactor is necessary to
properly support them without breaking other things.

Fixes wtype being completely broken.
2026-03-07 08:17:52 +03:00
Ivan Molodetskikh b82d52705e Increase default Wayland buffer size
The buffer size matches the one used in other compositors, e.g.:

- https://gitlab.gnome.org/GNOME/mutter/-/blob/49.4/src/wayland/meta-wayland.c?ref_type=tags#L970-971
2026-03-07 08:03:28 +03:00
Ivan Molodetskikh c7fa5f29d6 Upgrade dependencies 2026-03-07 08:03:24 +03:00
Jon Heinritz e708f54615 winit: add an app-id when running niri in nested mode in nested mode 2026-02-27 23:53:51 +08:00
phuongdpham 2dc6f4482c chore: update git ignore
+ Exclude .idea folder
2026-02-23 20:23:06 +08:00
Ivan Molodetskikh a2a5291175 wiki: Unbreak COPR links
Accidentally changed while doing a global replacement.
2026-02-17 08:17:55 +03:00
Tobias Heider 1fa0338a17 openbsd: implement close_range workaround
Since OpenBSD doesn't have close_range we have
to use close from and close the remaining
descriptors manually.
2026-02-16 03:00:17 +08:00
Christian Meissl 8e3e93b624 bump calloop to 0.14.4
resolves an issue where cancelling
timers can result in performance degradation
over time and memory buildup
2026-02-14 13:50:16 +08:00
Christian Meissl c1146c0bef bump smithay: fix zxdg_exported crash 2026-02-13 15:16:38 +08:00
Ivan Molodetskikh 41b5de8769 Change all links from YaLTeR/niri to niri-wm/niri 2026-02-10 17:59:14 +03:00
blue linden 8d9bc2a5c9 replace yalter/ and vortriz/ with niri-wm/ in repo links 2026-02-10 21:52:58 +08:00
Christian Meissl 6d5c5f12b2 Fix dead surface hook VRAM leak (#3404)
* remove pre-commit hook when surface is destroyed

this re-uses the already existing remove_default_dmabuf_pre_commit_hook
during surface destruction instead of just removing the hook from the
stored list of hooks.

* do not add dmabuf pre-commit hook for destroyed surfaces

this prevents surfaces getting stored indefinitely in case
some logic tries to add the hook for an already
destroyed surface.

* align surface/toplevel destruction order for client destruction

resource destruction has undefined order in case
the client does not explicitly destroy the resourced
and wait for destruction to complete.
the same applies for clients exiting unexpectedly.

* rearrange some things

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-02-10 05:55:49 +00:00
Ivan Molodetskikh 42b2aeb6e6 wiki: Add missing Since 2026-02-10 08:25:51 +03:00
Ivan Molodetskikh ab47f5cec4 Remove obsolete FIXME 2026-02-06 19:22:39 +03:00
ジムワルド 549148d277 Add load-config-file --path to load a different config (#3395)
* ipc: allow load-config to relocate the path of the config

* doc: add info about alternative configuration paths and relocating

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Update docs/wiki/Integrating-niri.md

* Update niri-ipc/src/lib.rs

* Update src/ipc/server.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-02-06 19:22:16 +03:00
Manuel Mendez 189917c933 Add trackball to Configuration:-Input overview 2026-02-03 12:29:25 +08:00
Ivan Molodetskikh f30db163b5 layout/tile: Remove redundant .to_f64() 2026-01-28 08:12:06 +03:00
Ivan Molodetskikh a78f07cd58 Remove ResolvedLayerRules::empty()
Same cleanup as ResolvedWindowRules earlier, but here we didn't even
have any reason to keep having it.
2026-01-28 08:12:06 +03:00
Semper_ 765a241c5a Link to Electron section of the wiki in FAQ (#3324)
* Update Electron info

There were changes made that remove the env variable:
https://github.com/electron/electron/issues/48001

* Clarify Electron versions

* Link to the electron section of the wiki

* Edit wording and link to the electron section of the wiki
2026-01-27 20:52:25 +03:00
Ivan Molodetskikh a00b271a15 pw_utils: Lower default buffer count to 8
We certainly don't need 16 buffers.
2026-01-27 20:42:25 +03:00
Semper_ e1015ac92f Docs: Update Electron info (#3320)
* Update Electron info

There were changes made that remove the env variable:
https://github.com/electron/electron/issues/48001

* Clarify Electron versions
2026-01-27 09:21:21 +03:00
Ivan Molodetskikh a34ed51586 Make debug_draw_opaque_regions work in screencasts again 2026-01-26 06:18:35 +03:00
Ivan Molodetskikh 5ddcf195dd Remove unused portable-atomic dep
Has been unused for a long while (since the animation clock refactor).
2026-01-25 20:49:07 +03:00
Ivan Molodetskikh e11abe554f Fix expel-window-from-column comment
It's been changed to this a while ago.
2026-01-25 13:33:15 +03:00
Ivan Molodetskikh 9261fd6342 layout/tests: Add test for second workspace y = 0 2026-01-25 18:28:35 +08:00
SAKURA fb2f66f361 layout/monitor: round workspace render geo to physical pixels 2026-01-25 18:28:35 +08:00
dependabot[bot] e2e15b7a18 build(deps): bump zbus in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [zbus](https://github.com/z-galaxy/zbus).


Updates `zbus` from 5.13.0 to 5.13.1
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus-5.13.0...zbus-5.13.1)

---
updated-dependencies:
- dependency-name: zbus
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 18:23:49 +08:00
Xarth 0a416eedda docs: Update Arch Linux installation instructions
Removed 'wl-clipboard' and 'cliphist' from the installation command for Arch Linux. Because dms doesn't request that now.
2026-01-25 18:22:38 +08:00
Ivan Molodetskikh d7184a04b9 render_helpers: Add Smithay Tracy GPU spans 2026-01-17 22:31:05 +03:00
Ivan Molodetskikh bdf394260a render_helpers: Add Tracy spans to draw() calls 2026-01-17 22:30:32 +03:00
Ivan Molodetskikh 74d14be01f Update Smithay (virtual keyboard, layer-shell geometry, GPU profiling)
Also includes the necessary code to handle the virtual keyboard
compositor-side. Similar to the virtual pointer, we have an InputDevice
impl that allows reusing the logic from process_input_event().

Co-authored-by: wxt <3264117476@qq.com>
2026-01-17 22:29:10 +03:00
Ivan Molodetskikh 3ccb06f564 Fix panic in screencopy manager destroyed() 2026-01-17 15:32:20 +03:00
Ivan Molodetskikh d9e755d575 screencasting: Only render pointer when it's within output 2026-01-17 13:59:23 +03:00
Ivan Molodetskikh 87e2dd0361 Revert "Move set_dynamic_cast_target() stub closer to the other ones"
This reverts commit dd93c39ed0.

Why did I do this, that function is on a different type
2026-01-15 17:29:37 +03:00
Ivan Molodetskikh dd93c39ed0 Move set_dynamic_cast_target() stub closer to the other ones 2026-01-15 13:14:23 +03:00
Ivan Molodetskikh 849788bb28 Add niri msg stop-cast --session-id 2026-01-15 13:13:50 +03:00
Ivan Molodetskikh 9015ff8e36 ipc: Add pw_node_id to PipeWire Casts 2026-01-15 08:42:25 +03:00
Ivan Molodetskikh e546b339a3 ipc: Add PID to screencopy Casts 2026-01-15 08:42:25 +03:00
Ivan Molodetskikh b39edf405a screencopy: Add timeout to casts considered stopped
Otherwise xdp-wlr never stops the cast after it first starts.
2026-01-15 08:42:25 +03:00
Ivan Molodetskikh b98f4906da ipc: Add CastKind 2026-01-15 08:42:25 +03:00
Ivan Molodetskikh e82830c68c ipc: Add screencopy cast tracking
Track wlr-screencopy sessions that use with_damage as screencasts. These
are used by tools like wl-screenrec for continuous recording.
2026-01-15 08:42:25 +03:00
Ivan Molodetskikh 238caaf8da ipc: Add screencast request and events for PipeWire casts
Allows desktop bars to show when screen recording is active.
2026-01-15 08:42:25 +03:00
Ivan Molodetskikh 9c79108afa Refactor wlr-screencopy state cleanup
Before we cleaned up when binding a new manager, meaning that after a
screencopy client exited, the queue kept existing until a new one is
bound. We'll need precise tracking for the screencast IPC, so this
commit refactors to do just that: clean up the queue immediately when
all referring objects no longer exist.

This commit also fixes an issue where destroyed frames (e.g. from a
killed client) didn't clean the corresponding screencopy objects,
leading them to exist forever.
2026-01-13 23:01:21 +03:00
Ivan Molodetskikh 2571242887 screencopy: Pop first screencopy instead of last
This was never found probably because no client submits multiple frames
at once.
2026-01-13 23:00:38 +03:00
Ivan Molodetskikh 6f92b3296a Store output name in CastTarget
Will be useful in the next commit to avoid fetching it every time.
2026-01-13 21:31:51 +03:00
Ivan Molodetskikh 570ea119ba Extract cast session/stream ID counters to global scope
Add CastSessionId and CastStreamId newtypes. This lifts the atomic
counters from the D-Bus mutter_screen_cast module to a shared location,
preparing for adding screencopy cast tracking which will need the same
ID types.
2026-01-13 21:31:51 +03:00
Ivan Molodetskikh df4614e62c screencasting: Use spans to reduce logging boilerplate 2026-01-12 21:33:29 +03:00
Ivan Molodetskikh 3672e79369 Delay starting dynamic casts until there's a target
This avoids a weird 1x1 stream as well as one renegotiation which is a
complex operation, and some clients apparently have a problem with it.
2026-01-12 08:45:03 +03:00
Ivan Molodetskikh 2d16abdaae Move dynamic_target set outside pw_utils 2026-01-12 08:37:58 +03:00
Ivan Molodetskikh ff081acddc screencasting: Extract some logic into functions 2026-01-12 08:37:58 +03:00
Ivan Molodetskikh afe27a143b Move xdp-gnome-screencast code into separate module 2026-01-12 07:59:56 +03:00
Ivan Molodetskikh fd2916eb72 Honor pointer visibility in screencasts
Regressed in 05599ce2c4.
2026-01-12 07:11:56 +03:00
Janis e9d888cd52 Set NIRI_BUILD_COMMIT in flake.nix (#3235) 2026-01-11 19:59:43 +03:00
abmantis 05599ce2c4 Implement cursor metadata in window screencast
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-01-11 06:51:14 -08:00
Ivan Molodetskikh 0fb6c5706b Extract pointer_pos_for_window_cast()
Will be used for window screencasts too.
2026-01-11 06:51:14 -08:00
Ivan Molodetskikh 79aaa4c6c0 Upgrade dependencies 2026-01-11 15:37:00 +03:00
Ivan Molodetskikh 7e559dc468 Use unadjusted clock for config notification shown duration
It shouldn't be affected by anim slowdown (or, more importantly,
speedup).
2026-01-11 15:08:58 +03:00
Ivan Molodetskikh 45fc763281 Update Insta snapshots for new Insta version
No functional changes intended.
2026-01-10 15:45:47 +03:00
Ivan Molodetskikh 39d3cd2415 Add a push version of render()
Will be useful for screencasting.
2026-01-10 15:32:21 +03:00
Ivan Molodetskikh 19b1074a8b Fix root surface tracking for sync subsurfaces
In particular, fixes screenshot-window with show-pointer on foot CSD.
2026-01-10 15:32:21 +03:00
Ivan Molodetskikh 539a5a8030 Fix tablet cursor for screenshot-window with pointer 2026-01-10 15:32:21 +03:00
Ivan Molodetskikh 53b7477d20 render_helpers: Fix encompassing_geo() argument type 2026-01-10 15:32:21 +03:00
Ivan Molodetskikh c34f7b18ec pick_color_grab: Remove unnecessary Vec allocation 2026-01-10 15:32:21 +03:00
Anton Kesy a6baef7b68 Fix typo 2026-01-10 04:30:42 -08:00
Manuel Romei 10df9f4717 fix(pw_utils): prevent write-after-free by reordering Cast struct fields
The Cast struct fields were ordered such that `stream` was dropped before
`_listener`. In Rust, struct fields are dropped in declaration order.

Because `StreamListener` attempts to unregister itself from the stream on
drop, and `StreamRc` destroys the underlying PipeWire stream on drop, the
previous order caused `_listener` to access the stream after it had
already been freed.

This reorders the fields so `_listener` is declared before `stream`,
ensuring the listener unregisters itself while the stream is still valid.

* Apply suggestion from @YaLTeR

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-01-07 13:32:06 +00:00
Ivan Molodetskikh 9f8eadc5bc Add screenshot-window show-pointer=true 2026-01-07 07:53:05 +03:00
Ivan Molodetskikh a496307daf Move pointer visibility check outside render_pointer() 2026-01-07 07:53:05 +03:00
Ivan Molodetskikh bc7bb51b6f Fix Tracy span name 2026-01-07 07:53:05 +03:00
Ivan Molodetskikh b7eb8a635b default-config: Bind Mod+M to maximize-window-to-edges 2026-01-05 10:28:18 +03:00
Ivan Molodetskikh d060b06667 Replace TODO with FIXME
We use TODO for things to be fixed before committing.
2026-01-05 08:29:05 +03:00
Ivan Molodetskikh 54c2e2ab47 utils/spawning: Remove unnecessary cfgs
Forgotten when this was refactored.
2026-01-04 15:43:42 +03:00
Ivan Molodetskikh df3f3979e9 layout/scrolling: Preserve gesture anim in dnd_scroll_gesture_end()
Fixes interactive move unmaximize/unfullscreen into floating skipping
the view offset anim.
2026-01-04 15:43:42 +03:00
Vishal 6215b5f0b1 doc: link DMS compositor setup in Quick Start (#3179)
* doc: add `dms setup` command in Getting Started section along with clarification

* mention DMS setup page

* Update docs/wiki/Getting-Started.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-01-04 15:04:22 +03:00
Mark Karlinsky 3bfa4a71ff Improve dinit service files (#3193)
* Improve dinit service files and niri-session

Two main changes were made:
 - After a discussion in davmac314/dinit#496, 2 dinit services are now
   provided. The first one is 'niri', which runs niri itself, and the
   second one is 'niri.target' which brings up all the dependences from
   user configuration.
 - Made the behaviour of 'niri-session' when running under dinit closer
   to the behaviour when running under systemd. In particular, now the
   script wait for service completion, because some login managers shut
   the session down the moment the startup script completes.

* Update paths in docs
2026-01-04 10:04:03 +03:00
LuckShiba 3158f5a9c0 nix: fix path replace in SystemD service 2026-01-03 21:43:13 +03:00
Axlefublr d8250fa876 niri.service: don't hardcode the path 2026-01-03 19:43:14 +03:00
Ivan Molodetskikh cf0b4bc0ca Fix missing redraw when floating DnD in overview scrolls workspaces
Regressed in 396097c3ab
2025-12-31 08:46:49 +03:00
Ivan Molodetskikh 1ab1737653 tty: Load libinput plugins if available
Some distros like Fedora build libinput with plugin autoloading, however
by default, the compositor needs to explicitly load them. Plus, we need
to load manually if we want to also load from
$XDG_CONFIG_HOME/libinput/plugins.

Ref. https://gitlab.gnome.org/GNOME/mutter/-/commit/c5b12fbf6313d51f3279901bb561023e56181e36
2025-12-30 08:22:11 +03:00
Ivan Molodetskikh b5640d5293 Don't add padding to layer-shell popups 2025-12-26 15:25:30 +03:00
Ivan Molodetskikh 860a08cce6 Update Smithay (text-input enter/leave fix, multigpu formats improvement) 2025-12-26 08:31:34 +03:00
Ivan Molodetskikh 2a9d0e495a Fix consume-or-expel-left anim to the left of active column
Regressed in c4462d0c7f.
2025-12-25 14:26:49 +03:00
Ivan Molodetskikh 7f132ecf95 Refactor rendering to push-based instead of pull-based (#3113)
Our current rendering code constructs and returns complex
`-> impl Iterator<Item = SomeRenderElement>` types that are collected
into a vector at the top level Niri::render(). This causes some
problems:
- It's hard to write logic around returning iterators. Especially things
  like conditions, since the returned iterator must have a single type,
  you can't branch and return different iterators. This will be solved
  by gen fn but alas it's not here yet.
- In many cases, the returned `-> impl Iterator` will borrow from &self
  leading to complex lifetimes. In certain cases, it is also desirable
  for it to borrow the &mut NiriRenderer, which causes a lot of issues
  because it's exclusive (&mut).
- Sometimes those issues are too hard to deal with, leading to the
  escape hatch of allocating and returning a temporary
  Vec<SomeRenderElement>, like in
  Scrolling/FloatingSpace::render_elements(). These allocations are
  unfortunate because they are not really necessary.
- It's impossible to use some downstream combinators with this
  `-> impl Iterator` approach, leading to functions like Smithay's
  render_elements_from_surface_tree() returning a Vec. This is extra
  unfortunate because it results in a temporary allocation per Wayland
  toplevel/popup.
- It's hard to properly create profiling spans for the rendering
  functions since the spans are dropped when the (lazy) iterator is
  returned and not when all the code actually completes.
- The code compiles down to complex state machines in generated iterator
  types with logic located in Iterator::next(), which makes it annoying
  to follow in debuggers and profiling tools.

This refactor changes the code to push-based iteration: rendering
functions receive a push() closure that they call to push their render
elements. It solves all of the aforementioned problems:
- The logic becomes simpler. Just use conditionals and loops as normal.
- No borrowing and lifetimes since we're not returning anything.
- All temporary Vecs are removed because the problems they worked around
  no longer exist.
- The new push_elements_from_surface_tree() helper is the same as
  render_elements_from_surface_tree() but doesn't allocate a temporary
  Vec since it's not necessary; the push() closure can be passed down.
- Profiling spans work normally since the function returns when it ran
  all of the logic.
- The code compiles down to normal functions and calls as expected.

Generally, the iterator approach gives these advantages:
- You can wrap the returned items in the upstream logic. This is
  possible in exactly the same way with the push closure.
- You can decide to cut the iterator short in the upstream logic. This
  is not possible with push-based iteration, but we don't actually use
  it anywhere.

I chose the push closure type to be &mut dyn FnMut(SomeRenderElement).
It's deliberately not a generic impl FnMut() to avoid duplicating the
rendering logic when it's called from several different places. But it's
still a normal closure that can capture the outside context.

While my original idea for this refactor was to simplify the logic while
getting rid of temporary Vecs, it also appears to have brought a
consistent 2-3x speedup to the whole render list construction. On an old
Eee PC laptop I even observed a 8x speedup.

The refactor also results in smaller binary size, presumably due to
removing many iterator combinators and state tracking.
2025-12-25 14:26:19 +03:00
Ivan Molodetskikh 1a63089d67 Fix tracy span names 2025-12-25 09:52:50 +03:00
Ivan Molodetskikh 88dc6e22d0 Remove redundant clippy allow 2025-12-25 09:42:08 +03:00
Ivan Molodetskikh ce8171bed3 Fix wrong rendering order when switching dynamic cast to window 2025-12-25 08:51:43 +03:00
Ivan Molodetskikh 6edd29170f opening window: Remove unused method 2025-12-25 08:51:43 +03:00
Ivan Molodetskikh 9d62b94688 scrolling: Don't forget to call tab_indicator.update_shaders()
This didn't actually break anything since those shaders aren't
configurable.
2025-12-25 08:51:43 +03:00
Ivan Molodetskikh 4d295418ce clipped surface: Compute uniforms on-demand
Removes two allocations for every clipped surface.
2025-12-23 12:51:59 +03:00
HigherOrderLogic f01d48bc51 ci: user Cachix nix installer 2025-12-23 10:27:32 +03:00
HigherOrderLogic 31ca509160 ci: remove flake check action 2025-12-23 10:27:32 +03:00
Ivan Molodetskikh 396097c3ab Fix constant repaint in the open overview 2025-12-23 08:51:54 +03:00
Ivan Molodetskikh ad62c8e487 gradient_fade: Store uniform inline 2025-12-23 07:50:58 +03:00
Ivan Molodetskikh 9e73beb165 shader: Store uniforms in Rc instead of Vec
It's frequently cloned (e.g. every border piece every render) and we
don't change it.
2025-12-23 07:50:58 +03:00
Ivan Molodetskikh 4fca614510 Update Smithay (DnD rework fix, dmabuf and geometry improvements) 2025-12-23 07:50:58 +03:00
Ivan Molodetskikh 19e55a2df0 Don't override IME grab with popup keyboard grab
Fixes menu in Telegram. Some weird behavior is still possible e.g. with
gtk4-widget-factory and dropdowns on entries, but things seem to be
slightly less broken this way.
2025-12-20 14:11:02 +03:00
Ivan Molodetskikh 6472209b45 Comment out spammy trace!() 2025-12-20 14:08:48 +03:00
Ivan Molodetskikh d9ceff7c70 Remove IME grab check, fix GTK 4 popups with IME
The wording in the deleted comment still stands: Smithay doesn't handle
overlapping grabs. However, in this case things appear to more or less
work themselves out. IME seems to re-request its grab every time an
input field is focused, replacing the popup keyboard grab. And the popup
keyboard grab doesn't seem to mind being replaced this way.
2025-12-20 13:46:59 +03:00
Ivan Molodetskikh 813c5ee05f Warp pointer across the screen during spatial movement grabs 2025-12-20 10:50:07 +03:00
Ivan Molodetskikh 47e217c00e Use relative motion in move and spatial movement grab
Will be used for pointer warping.
2025-12-20 10:49:06 +03:00
Ivan Molodetskikh 9b52465e42 layout: Synchronize unfullscreen view movement anim to resize
Before this commit, maximize/fullscreen was synchronized, but
unmaximize/unfullscreen wasn't.
2025-12-20 09:08:17 +03:00
Ivan Molodetskikh 7d60231e35 wiki: Clarify that environment isn't imported to systemd 2025-12-20 08:33:02 +03:00
John Rinehart 7a237e519c Implement include optional=true (#3022)
* feat(niri): support `include optional=true "filename.kdl"`

* chore: warn if optional include ENOENT

* chore: validate include directive arguments and properties

Add proper validation to reject:
- Extra arguments beyond the path
- Unknown properties (other than "optional")
- Unexpected child nodes

* docs: implement suggested typographical/prose changes

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-12-20 05:04:18 +00:00
Ivan Molodetskikh c4462d0c7f layout/scrolling: Fix add_column() skipping activate_column() sometimes
When the column was added immediately to the left of the current column
and activated, the new idx would be equal to active_column_idx, which
would skip activate_column() with its variable resets.
2025-12-18 22:19:03 +03:00
Ivan Molodetskikh f85cb5c5f9 dependabot: Add cooldown 2025-12-18 13:39:30 +03:00
Ivan Molodetskikh 7ca46b44b2 Update Smithay (DnD rework, primary GPU improvement) 2025-12-18 13:17:35 +03:00
Ivan Molodetskikh f913219f94 Use is_none_or() 2025-12-18 11:54:07 +03:00
Ivan Molodetskikh 80469abc20 Bump MSRV to 1.85, upgrade deps 2025-12-18 11:54:07 +03:00
Kirill Chibisov 890935d2ba Use Grabbing cursor for Mod+LMB interactive move (#3045)
* Use Grabbing cursors for interactive move

There was no real indication that something can be dragged and thus
it's generally harder to discover for someone not familiar with Mod+LMB
to start dragging window around.

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-12-18 08:07:24 +03:00
Ivan Molodetskikh d2fa1f54d4 Add force-disable-connectors-on-resume debug flag 2025-12-18 07:39:44 +03:00
Ivan Molodetskikh 2641356d41 mru: Don't handle pointer input until visible 2025-12-16 08:05:51 +03:00
Ivan Molodetskikh 7c0898570c Remove url dependency
Just use the glib function.

Turns out url comes with a huge dep tree. Well, I guess back when I
wrote this, we didn't have glib in our deps, but we had for a long time.
2025-12-14 07:50:00 +03:00
Ivan Molodetskikh d1fc1ab731 CI/freebsd: Fix PW patch application 2025-12-13 14:39:57 +03:00
Ivan Molodetskikh d9a9e6ddc4 CI: Remove Rust install from FreeBSD action
We don't need it since we removed the cache.
2025-12-13 14:27:21 +03:00
Ivan Molodetskikh 0cb20b55b8 CI: Update FreeBSD to 15.0 2025-12-13 14:26:17 +03:00
Ivan Molodetskikh 3d2d7b95d9 CI: Re-enable FreeBSD 2025-12-13 14:23:32 +03:00
Ivan Molodetskikh c22d8358c2 wiki/packaging: Mention recommended deps 2025-12-12 10:26:13 +03:00
Ivan Molodetskikh 4d058e6111 rpkg: Add explicit libwayland-server dependency 2025-12-09 22:02:36 +03:00
DerRockWolf 83a733e085 Update issue template to put niri config into <details> block
This makes issues much more readable and prevents readers from needing to scroll all the way past the config.
2025-12-09 07:55:53 +03:00
Ivan Molodetskikh ba29735fbb contributing: Add a section on how to get PR reviewed more quickly 2025-12-05 23:21:54 +03:00
Ivan Molodetskikh 6fc092cc4f contributing: Add a section on AI contributions 2025-12-05 23:21:47 +03:00
Robert Gu f874b2fce5 Update Integrating-niri.md on multi-file configs (#2943)
* Update Integrating-niri.md on multi-file configs

* Apply suggestion from @YaLTeR

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-12-02 15:23:01 +03:00
Semper_ 311ca6b5da Docs: add a few notes and warnings (#2925)
* update docs.

* Update Xwayland.md.

* Apply suggestion from @YaLTeR

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-11-30 09:51:13 +03:00
1134 changed files with 11357 additions and 5036 deletions
+7
View File
@@ -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.
+2 -2
View File
@@ -1,9 +1,9 @@
contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
url: https://github.com/niri-wm/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
- name: Ask a question
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
url: https://github.com/niri-wm/niri/discussions/new?category=q-a
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
+4 -2
View File
@@ -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
+20 -27
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-24.04
container: alpine:3
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -121,7 +121,7 @@ jobs:
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -148,7 +148,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -172,7 +172,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -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
@@ -195,7 +195,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -217,7 +217,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -230,10 +230,10 @@ jobs:
fedora:
runs-on: ubuntu-24.04
container: fedora:41
container: fedora:42
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -246,38 +246,35 @@ 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
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
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 \
@@ -292,16 +289,12 @@ jobs:
dotnet: false
large-packages: false
- uses: actions/checkout@v4
- uses: actions/checkout@v6
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
@@ -315,7 +308,7 @@ jobs:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true
show-progress: false
@@ -332,7 +325,7 @@ jobs:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true
show-progress: false
+7 -4
View File
@@ -22,15 +22,18 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
# Fail if a match is found (exit code 0)
grep --recursive 'Since: next release' docs/wiki && exit 1
# Fail if grep failed (exit code 2)
status=$?
if [ $status -ne 1 ]; then exit $status; fi
- name: Install dependencies
run: |
+2
View File
@@ -1,2 +1,4 @@
/target
/result
.idea
+20 -2
View File
@@ -31,7 +31,7 @@ I would really appreciate help with testing and reviewing them.
### Testing
Pick a pull request you like, then build it and give it a go.
The [Developing niri wiki page](https://yalter.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
The [Developing niri wiki page](https://niri-wm.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
Be really thorough with your testing.
We're striving for polished features in niri, so point out any issues and bugs, even small ones like animation jank.
@@ -84,12 +84,30 @@ When creating pull requests, please keep the following in mind.
- When working on bigger features, I usually start with a big messy commit, then gradually split out smaller self-contained changes from it as the code gets into shape.
- [git-rebase.io](https://git-rebase.io/) is a helpful guide for splitting commits and cleaning up history in git.
- When you address a review comment, mark it as resolved.
- Remember to [run tests](https://yalter.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
- Remember to [run tests](https://niri-wm.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
- For new layout actions, remember to add them to the randomized tests. For weird Wayland handling, adding client-server tests in `src/tests/` could be very useful.
- Test your changes by hand thoroughly, including for edge cases and weird interactions. See the Testing section above for some tips.
- 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
+1077 -1276
View File
File diff suppressed because it is too large Load Diff
+51 -47
View File
@@ -6,37 +6,35 @@ members = [
]
[workspace.package]
version = "25.11.0"
version = "26.4.0"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.80.1"
repository = "https://github.com/niri-wm/niri"
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"
anyhow = "1.0.102"
bitflags = "2.11.1"
clap = { version = "4.6.1", features = ["derive"] }
insta = "1.47.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
tracing = { version = "0.1.41", 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 }
serde_json = "1.0.149"
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracy-client = { version = "0.18.4", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
# path = "../smithay/smithay-drm-extras"
[package]
@@ -53,51 +51,53 @@ 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_unix 0.18 has a regression where it doesn't work in normal configurations.
# accesskit 0.21 is its correct dependent version.
# https://github.com/niri-wm/niri/issues/3594
accesskit = { version = "0.21", optional = true }
accesskit_unix = { version = "0.17", optional = true }
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.5.0"
async-io = { version = "2.6.0", optional = true }
atomic = "0.6.1"
bitflags.workspace = true
bytemuck = { version = "1.23.2", features = ["derive"] }
calloop = { version = "0.14.3", features = ["executor", "futures-io", "signals"] }
bytemuck = { version = "1.25.0", features = ["derive"] }
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
clap = { workspace = true, features = ["string"] }
clap_complete = "4.5.58"
clap_complete_nushell = "4.5.8"
clap_complete = "4.6.2"
clap_complete_nushell = "4.6.0"
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"] }
drm-ffi = "0.9.1"
fastrand = "2.4.1"
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.30.8"
input = { version = "0.9.1", features = ["libinput_1_21"] }
glam = "0.32.1"
input = { version = "0.10.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.176"
libc = "0.2.185"
libdisplay-info = "0.3.0"
log = { version = "0.4.28", 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"
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "26.4.0", path = "niri-config" }
niri-ipc = { version = "26.4.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.3.0"
pango = { version = "0.21.5", features = ["v1_44"] }
pangocairo = "0.21.5"
pipewire = { version = "0.9.2", optional = true, features = ["v0_3_33"] }
png = "0.18.0"
portable-atomic = { version = "1.11.1", default-features = false, features = ["float"] }
png = "0.18.1"
profiling = "1.0.17"
sd-notify = "0.4.5"
sd-notify = "0.5.0"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.7", optional = true }
wayland-backend = "0.3.11"
wayland-scanner = "0.31.7"
wayland-backend = "0.3.15"
wayland-scanner = "0.31.10"
wayland-server = "0.31.13"
xcursor = "0.3.10"
zbus = { version = "5.11.0", optional = true }
zbus = { version = "5.13.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -121,22 +121,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"] }
rayon = "1.11.0"
wayland-client = "0.31.11"
proptest = "1.11.0"
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
rayon = "1.12.0"
wayland-client = "0.31.14"
xshell = "0.2.7"
[build-dependencies]
pkg-config = "0.3.33"
[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.
@@ -146,6 +149,7 @@ dinit = []
[lints.clippy]
new_without_default = "allow"
collapsible_match = "allow"
[profile.release]
debug = "line-tables-only"
@@ -161,7 +165,7 @@ insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "25.11"
version = "26.04"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+16 -16
View File
@@ -2,12 +2,12 @@
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
<a href="https://github.com/niri-wm/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/niri-wm/niri"></a>
<a href="https://github.com/niri-wm/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/niri-wm/niri?logo=github"></a>
</p>
<p align="center">
<a href="https://yalter.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://yalter.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
<a href="https://niri-wm.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://niri-wm.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/niri-wm/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
@@ -29,23 +29,23 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://yalter.github.io/niri/Workspaces.html) like in GNOME
- [Dynamic workspaces](https://niri-wm.github.io/niri/Workspaces.html) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://yalter.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://yalter.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://yalter.github.io/niri/Tabs.html)
- You can [block out](https://niri-wm.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://niri-wm.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/niri-wm/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/niri-wm/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://niri-wm.github.io/niri/Tabs.html)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://yalter.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
- [Animations](https://github.com/niri-wm/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/niri-wm/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- Live-reloading config
- Works with [screen readers](https://yalter.github.io/niri/Accessibility.html)
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
@@ -55,7 +55,7 @@ Niri is stable for day-to-day use and does most things expected of a Wayland com
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://yalter.github.io/niri/Getting-Started.html) page.
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Also check out [awesome-niri], a list of niri-related links and projects.
@@ -72,7 +72,7 @@ We have touchpad gestures, but no touchscreen gestures yet.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: [integrated](https://yalter.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
- **Xwayland**: [integrated](https://niri-wm.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
## Media
@@ -93,7 +93,7 @@ An LWN article with a nice overview and introduction to niri.
## Contributing
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
## Inspiration
@@ -121,7 +121,7 @@ We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
[PaperWM]: https://github.com/paperwm/PaperWM
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[awesome-niri]: https://github.com/Vortriz/awesome-niri
[awesome-niri]: https://github.com/niri-wm/awesome-niri
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
+10
View File
@@ -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
View File
@@ -40,5 +40,5 @@ def _badge_for_version(preposition: str, version: str):
# we might fail to make real links to release notes on other cases too, but for now this is the one i've found
return f"<span class=\"badge\">{preposition}: {version}</span>"
else:
path = f"https://github.com/YaLTeR/niri/releases/tag/v{version}"
path = f"https://github.com/niri-wm/niri/releases/tag/v{version}"
return f"<span class=\"badge\">[{preposition}: {version}]({path})</span>"
+3 -2
View File
@@ -1,7 +1,7 @@
site_name: niri
docs_dir: wiki
site_url: https://yalter.github.io/niri
repo_url: https://github.com/YaLTeR/niri
site_url: https://niri-wm.github.io/niri
repo_url: https://github.com/niri-wm/niri
edit_uri: edit/main/docs/wiki/
use_directory_urls: false
@@ -85,6 +85,7 @@ nav:
- Xwayland: Xwayland.md
- Gestures: Gestures.md
- Fullscreen and Maximize: Fullscreen-and-Maximize.md
- Window Effects: Window-Effects.md
- Packaging niri: Packaging-niri.md
- Integrating niri: Integrating-niri.md
- Accessibility: Accessibility.md
+1 -1
View File
@@ -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" }
+16 -2
View File
@@ -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.
+16
View File
@@ -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,21 @@ debug {
}
```
### `force-disable-connectors-on-resume`
<sup>Since: 26.04</sup>
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
This causes a modeset/screen blank on all outputs.
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.
+30
View File
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
Included config files can in turn include more files.
All included files are watched for changes, and the config live-reloads when any of them change.
You can include by filename or path.
* Relative to the current file: `other.kdl` or `./other.kdl`
* By absolute path: `/path/to/file.kdl`
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
Includes work only at the top level of the config:
```kdl,must-fail
@@ -114,6 +120,30 @@ window-rule {
}
```
### Optional includes
<sup>Since: 26.04</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.
+6 -1
View File
@@ -2,7 +2,7 @@
In this section you can configure input devices like keyboard and mouse, and some input-related options.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `trackball`, `tablet`, `touch`.
Settings in those sections will apply to every device of that type.
Currently, there's no way to configure specific devices individually (but that is planned).
@@ -89,6 +89,7 @@ input {
tablet {
// off
map-to-output "eDP-1"
// map-to-focused-output
// left-handed
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
}
@@ -281,6 +282,10 @@ Valid output names are the same as the ones used for output configuration.
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
Setting specific to `tablet`:
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
### General Settings
These settings are not specific to a particular input device.
+1 -1
View File
@@ -19,7 +19,7 @@ You can find documentation for various sections of the config on these wiki page
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
+11
View File
@@ -382,6 +382,17 @@ binds {
}
```
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
```kdl
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>
+101
View File
@@ -14,6 +14,7 @@ Here are all matchers and properties that a layer rule could have:
layer-rule {
match namespace="waybar"
match at-startup=true
match layer="top"
// Properties that apply continuously.
opacity 0.5
@@ -34,6 +35,25 @@ layer-rule {
geometry-corner-radius 12
place-within-backdrop true
baba-is-float true
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
popups {
opacity 0.5
geometry-corner-radius 6
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
}
}
```
@@ -69,6 +89,22 @@ layer-rule {
}
```
#### `layer`
<sup>Since: 26.04</sup>
Matches surfaces on this layer-shell layer.
Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
```kdl
// Make all overlay-layer surfaces FLOAT.
layer-rule {
match layer="overlay"
baba-is-float true
}
```
### Dynamic Properties
These properties apply continuously to open layer-shell surfaces.
@@ -191,3 +227,68 @@ layer-rule {
baba-is-float true
}
```
#### `background-effect`
<sup>Since: 26.04</sup>
Override the background effect options for this surface.
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
- `blur`: set to `true` to enable blur behind this surface, or `false` to force-disable it.
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// Make top and overlay layers use the regular blur (if enabled),
// while bottom and background layers keep using the efficient xray blur.
layer-rule {
match layer="top"
match layer="overlay"
background-effect {
xray false
}
}
```
#### `popups`
<sup>Since: 26.04</sup>
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
The properties work the same way as the corresponding layer-rule properties, except that they apply to the layer surface's pop-ups rather than to the layer surface itself.
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
Other properties apply independently.
> [!NOTE]
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
>
> Some desktop shells will emulate pop-ups by drawing something that looks like a pop-up inside a regular layer surface.
> As far as niri is concerned, those are just layer surfaces and not pop-ups, so this block won't apply to them.
>
> This block also does not affect input-method pop-ups, such as Fcitx.
```kdl
// Blur the background behind Waybar popup menus.
layer-rule {
match namespace="^waybar$"
popups {
// Match the default GTK 3 popup corner radius.
geometry-corner-radius 6
opacity 0.85
background-effect {
blur true
}
}
}
```
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the layer surface correctly sets its Wayland geometry to exclude any shadows.
Pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
+3 -1
View File
@@ -177,6 +177,7 @@ layout {
### `preset-column-widths`
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
`proportion` sets the width as a fraction of the output width, taking gaps into account.
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
@@ -228,7 +229,8 @@ layout {
<sup>Since: 0.1.9</sup>
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
`proportion` sets the height as a fraction of the output height, taking gaps into account.
The default preset heights are <sup>1</sup>&frasl;<sub>3</sub>, <sup>1</sup>&frasl;<sub>2</sub> and <sup>2</sup>&frasl;<sub>3</sub> of the output.
+94
View File
@@ -54,6 +54,14 @@ hotkey-overlay {
config-notification {
disable-failed
}
blur {
// off
passes 3
offset 3.0
noise 0.02
saturation 1.5
}
```
### `spawn-at-startup`
@@ -141,6 +149,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.
@@ -313,3 +328,82 @@ config-notification {
disable-failed
}
```
### `blur`
<sup>Since: 26.04</sup>
Blur configuration that affects all background blur.
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// These are the default values:
blur {
// off
passes 3
offset 3
noise 0.02
saturation 1.5
}
```
#### `off`
By default, blur is available on request by a window or layer surface (via the `ext-background-effect` protocol).
You can also enable it manually with the `blur true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Setting the `off` flag will disable all blur, both requested by the window, and configured in window rules.
```kdl
blur {
off
}
```
#### `passes` and `offset`
`passes` controls the number of downsample/upsample passes for dual kawase blur.
More passes produce a larger, smoother blur, but cost more GPU resources.
`offset` is the pixel offset multiplier for each pass.
Offset `1` is the original dual kawase blur.
Larger values produce a smoother blur, at no additional GPU cost.
However, setting `offset` too big will produce visual artifacts.
You will need to increase `passes` to be able to use a bigger `offset` without artifacts.
When configuring blur, try increasing `offset` first (since it doesn't cause any extra GPU load) until you start getting artifacts.
Then, if you still need smoother blur, increase `passes` by 1.
Keep doing this until you get the desired visuals.
```kdl
blur {
passes 3
offset 3.0
}
```
#### `noise`
Amount of noise to add on top of the blur.
This is helpful to reduce color banding artifacts.
```kdl
blur {
noise 0.02
}
```
#### `saturation`
Color saturation applied to the blurred background.
Values above `1` increase saturation; values below `1` reduce it.
```kdl
blur {
saturation 1.5
}
```
@@ -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"; }
+108
View File
@@ -100,6 +100,25 @@ window-rule {
tiled-state true
baba-is-float true
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
popups {
opacity 0.5
geometry-corner-radius 15
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
}
min-width 100
max-width 200
min-height 300
@@ -909,6 +928,95 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
</video>
#### `background-effect`
<sup>Since: 26.04</sup>
Override the background effect options for this window.
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
- `blur`: set to `true` to enable blur behind this window, or `false` to force-disable it.
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// Make floating windows use the regular blur (if enabled),
// while tiled windows keep using the efficient xray blur.
//
// Warning: non-xray blur is currently experimental and has known limitations.
// In particular, it doesn't work during window opening and closing animations.
window-rule {
match is-floating=true
background-effect {
xray false
}
}
```
#### `popups`
<sup>Since: 26.04</sup>
Override properties for this window's pop-ups (menus and tooltips).
The properties work the same way as the corresponding window-rule properties, except that they apply to the window's pop-ups rather than to the window itself.
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
Other properties apply independently.
> [!NOTE]
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
>
> Examples of things that look like pop-ups that won't work:
>
> - Fully emulated by the client, i.e. not a pop-up at all, the client just draws something that looks like a pop-up inside its window.
> These are common in game engines and in web apps, e.g. the right click menu in Google Docs or in Electron apps like Discord.
>
> - Uses a wl-subsurface instead of an xdg-popup.
> Common in older apps using GTK 3, notably Firefox still uses these for some menus.
> Subsurfaces are an indivisible part of a surface and they aren't usually pop-ups, so it wouldn't make sense for niri to apply these rules to them.
>
> These emulated pop-ups come with other downsides: they cannot reliably extend outside their window, and if the app tries to do that, they will be clipped by rules such as `clip-to-geometry`.
> So most modern apps will correctly use xdg-popup, which is the intended way to show pop-ups on Wayland.
>
> This block also does not affect input-method pop-ups, such as Fcitx.
>
> For pop-ups created by your desktop shell or desktop components, use the corresponding [layer rule](./Configuration:-Layer-Rules.md#popups).
```kdl
// Blur the background behind pop-up menus in Nautilus.
window-rule {
match app-id="Nautilus"
popups {
// Matches the default libadwaita pop-up corner radius.
geometry-corner-radius 15
// Note: it'll look better to set background opacity
// through your GTK theme CSS and not here.
// This is just an example that makes it look obvious.
opacity 0.5
background-effect {
blur true
}
}
}
```
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the window correctly sets its Wayland geometry to exclude any shadows.
For example, GTK 4 pop-ups with pointing arrows (`has-arrow=true` property) are *not* rounded rectangles—the arrow sticks out—so if you enable blur, it will also stick out of the pop-up.
| Correct | Wrong |
|-----------------------------------------------------|--------------------------------------------------------------------------------|
| The pop-up is a rounded rectangle. Blur looks fine. | The pop-up is not a rounded rectangle. Blur extends above, where the arrow is. |
| ![](./img/popup-no-arrow.png) | ![](./img/popup-arrow.png) |
These pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
#### Size Overrides
You can amend the window's minimum and maximum size in logical pixels.
+1 -1
View File
@@ -74,7 +74,7 @@ Here are some design considerations for the window layout logic.
## Default config
The [default config](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
The [default config](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
Importantly, it is not a "suggested rice config"; we don't want to startle people with full-on rainbow borders and crazy shaders.
Since we're not a complete desktop environment (and don't have the contributor base to become one), we cannot provide a fully integrated experience—distro spins are better positioned to do this.
+2 -2
View File
@@ -1,8 +1,8 @@
niri's documentation files are found in `docs/wiki/` and should be viewable and browsable in at least three systems:
- The GitHub repo's markdown file preview
- [The GitHub repo's wiki](https://github.com/YaLTeR/niri/wiki)
- [The documentation site](https://yalter.github.io/niri/)
- [The GitHub repo's wiki](https://github.com/niri-wm/niri/wiki)
- [The documentation site](https://niri-wm.github.io/niri/)
## The GitHub repo's wiki
+20 -8
View File
@@ -40,12 +40,26 @@ hotkey-overlay {
}
```
### How to fix lag on external monitors connected to a hybrid GPU laptop?
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
### How to run X11 apps like Steam or Discord?
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
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?
@@ -66,14 +80,12 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
### Can I enable blur behind semitransparent windows?
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/54).
There's also [a PR](https://github.com/YaLTeR/niri/pull/1634) adding blur to niri which you can build and run manually.
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
<sup>Since: 26.04</sup> Yes.
See the [window effects](./Window-Effects.md) wiki page.
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/932).
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/932).
You can emulate this with a script that uses the niri IPC.
For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature (`toggle-follow-mode`).
@@ -82,7 +94,7 @@ For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature
Firefox seems to first open the Bitwarden window with a generic Firefox title, and only later change the window title to Bitwarden, so you can't effectively target it with an `open-floating` window rule.
You'll need to use a script, for example [this one](https://github.com/YaLTeR/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
You'll need to use a script, for example [this one](https://github.com/niri-wm/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
### Can I open a window directly in the current column / in the same column as another window?
@@ -92,7 +104,7 @@ Listen to the event stream for a new window opening, then call an action like `c
Adding this directly to niri is challenging:
- The act of "opening a window directly in some column" by itself is quite involved. Niri will have to compute the exact initial window size provided how other windows in a column would resize in response. This logic exists, but it isn't directly pluggable to the code computing a size for a new window. Then, it'll need to handle all sorts of edge cases like the column disappearing, or new windows getting added to the column, before the target window had a chance to appear.
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/YaLTeR/niri/discussions/1125.
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/niri-wm/niri/discussions/1125.
### Why does moving the mouse against a monitor edge focus the next window, but only sometimes?
+1 -1
View File
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
<sup>Since: 25.11</sup>
You can maximize an individual window via `maximize-window-to-edges`.
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
You will still see your bar, but not struts, gaps, or borders.
+7 -9
View File
@@ -9,10 +9,9 @@ sudo dnf install niri dms
systemctl --user add-wants niri.service dms
```
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
Arch Linux:
```
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
paru -S dms-shell-bin matugen wl-clipboard cliphist cava qt6-multimedia-ffmpeg
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
systemctl --user add-wants niri.service dms
```
@@ -29,6 +28,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.
@@ -145,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
@@ -223,7 +221,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
+2 -2
View File
@@ -26,7 +26,7 @@ To get a taste of the events, run `niri msg event-stream`.
Though, this is more of a debug function than anything.
You can get raw events from `niri msg --json event-stream`, or by connecting to the niri socket and requesting an event stream manually.
You can find the full list of events along with documentation [here](https://yalter.github.io/niri/niri_ipc/enum.Event.html).
You can find the full list of events along with documentation [here](https://niri-wm.github.io/niri/niri_ipc/enum.Event.html).
### Programmatic Access
@@ -57,7 +57,7 @@ $ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2
{"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}
```
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://yalter.github.io/niri/niri_ipc/).
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://niri-wm.github.io/niri/niri_ipc/).
### Backwards Compatibility
+3
View File
@@ -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).
+7 -3
View File
@@ -4,14 +4,18 @@ First, for creating a niri package, see the [Packaging](./Packaging-niri.md) pag
### Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
This means that you can customize your distribution defaults by creating `/etc/niri/config.kdl`.
When this file is present, niri *will not* automatically create a config at `~/.config/niri/`, so you'll need to direct your users how to do it themselves.
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.
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
### Xwayland
@@ -32,7 +36,7 @@ Make sure your system installer sets the keyboard layout via systemd-localed, an
### Autostart
Niri works with the normal systemd autostart.
The default [niri.service](https://github.com/YaLTeR/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
The default [niri.service](https://github.com/niri-wm/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
To make a program run at niri startup without editing the niri config, you can either link its .desktop to `~/.config/autostart/`, or use a .service file with `WantedBy=graphical-session.target`.
See the [example systemd setup](./Example-systemd-Setup.md) page for some examples.
+20 -1
View File
@@ -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/niri-wm/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.
+2 -2
View File
@@ -4,9 +4,9 @@ Feel free to look through usage and [Getting started](./Getting-Started.md).
If you're looking for ways to configure niri, check out the [introduction to configuration](./Configuration:-Introduction.md).
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
If you're not already here, check out our new wiki website! https://yalter.github.io/niri/
If you're not already here, check out our new wiki website! https://niri-wm.github.io/niri/
---
+85
View File
@@ -0,0 +1,85 @@
### Overview
<sup>Since: 26.04</sup>
You can apply background effects to windows and layer-shell surfaces.
These include blur, xray, saturation, and noise.
They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules.
The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window).
Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this.
### Blur
Windows and layer surfaces can request their background to be blurred via the [`ext-background-effect` protocol](https://wayland.app/protocols/ext-background-effect-v1).
In this case, the application will usually offer some "background blur" setting that you'll need to enable in its configuration.
You can also enable blur on the niri side with the `blur true` background effect window rule:
```kdl
// Enable blur behind the Alacritty terminal.
window-rule {
match app-id="^Alacritty$"
background-effect {
blur true
}
}
// Enable blur behind the fuzzel launcher.
layer-rule {
match namespace="^launcher$"
background-effect {
blur true
}
}
```
Blur enabled via the window rule will follow the window corner radius set via [`geometry-corner-radius`](./Configuration:-Window-Rules.md#geometry-corner-radius).
On the other hand, blur enabled through `ext-background-effect` will exactly follow the shape requested by the window.
If the window or layer has clientside rounded corners or other complex shape, it should set a corresponding blur shape through `ext-background-effect`, then it will get correctly shaped background blur without any manual niri configuration.
Windows can also blur their pop-up menus using `ext-background-effect`.
On the niri side, you can do it with a `popups` block inside [`window-rule`](./Configuration:-Window-Rules.md#popups) and [`layer-rule`](./Configuration:-Layer-Rules.md#popups).
See those wiki pages for examples and limitations.
Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
### Xray
Xray makes the window background "see through" to your wallpaper, ignoring any other windows below.
You can enable it with `xray true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Xray is automatically enabled by default if any other background effect (like blur) is active.
This is because it's much more efficient: with xray active, niri only needs to blur the background once, and then can reuse this blurred version with no extra work (since the wallpaper changes very rarely).
If you have an animated wallpaper, xray will still have to recompute blur every frame, but that happens once and shared among all windows, rather than recomputed separately for each window.
#### Non-xray effects (experimental)
You can disable xray with `xray false` background effect window rule.
This gives you the normal kind of blur where everything below a window is blurred.
Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
> [!WARNING]
> Non-xray effects are currently experimental because they have some known limitations.
>
> - They disappear during window open/close animations and while dragging a tiled window.
> Fixing this requires a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
### Implementation notes
The `ext-background-effect` protocol supports any wl_surface.
We currently implement it only for toplevels, layer surfaces, and pop-ups, which should cover the vast majority of what's actually used by applications.
For pop-ups, effects default to *non-xray* because pop-ups generally appear on top of windows.
In particular, the following surface types don't support `ext-background-effect`.
They can be implemented as the need arises.
- Subsurfaces. Would require implementing `clip-to-geometry` support for background effects.
- Lock surfaces. Not useful as it would just show our red locked session background.
- Cursor and drag-and-drop icon.
The main challenge here will be screencasts where the cursor is rendered separately.
This is problematic because non-xray effects require rendering the whole scene in one go rather than separately.
+1 -1
View File
@@ -47,7 +47,7 @@ It will open as a new window.
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
![Xwayland running in rootful mode.](https://github.com/YaLTeR/niri/assets/1794388/b64e96c4-a0bb-4316-94a0-ff445d4c7da7)
![Xwayland running in rootful mode.](https://github.com/niri-wm/niri/assets/1794388/b64e96c4-a0bb-4316-94a0-ff445d4c7da7)
Here's how you do it:
+1
View File
@@ -14,6 +14,7 @@
* [Xwayland](./Xwayland.md)
* [Gestures](./Gestures.md)
* [Fullscreen and Maximize](./Fullscreen-and-Maximize.md)
* [Window Effects](./Window-Effects.md)
* [Packaging niri](./Packaging-niri.md)
* [Integrating niri](./Integrating-niri.md)
* [Accessibility](./Accessibility.md)
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5a63ea3cc2f158e175c00dd058988a2bbf676e2a2aac5c2ef1603bd983589d5
size 166777
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef0c57d617916bf6014fe08e268c8201d7f6ef682e3aea3395e76116b1d0400
size 56936
+6 -4
View File
@@ -20,6 +20,7 @@
rust-overlay,
}:
let
revision = self.shortRev or self.dirtyShortRev or "unknown";
niri-package =
{
lib,
@@ -46,7 +47,7 @@
rustPlatform.buildRustPackage {
pname = "niri";
version = self.shortRev or self.dirtyShortRev or "unknown";
version = revision;
src = lib.fileset.toSource {
root = ./.;
@@ -64,7 +65,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 = {
@@ -107,7 +108,7 @@
buildNoDefaultFeatures = true;
# ever since this commit:
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# https://github.com/niri-wm/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
# and thus creates a real socket file in the runtime dir.
# this is fine for our build, we just need to make sure it has a directory to write to.
@@ -148,6 +149,7 @@
"-Wl,--pop-state"
]
);
NIRI_BUILD_COMMIT = revision;
};
passthru = {
@@ -156,7 +158,7 @@
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/YaLTeR/niri";
homepage = "https://github.com/niri-wm/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
+3 -3
View File
@@ -9,11 +9,11 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.7.2"
csscolorparser = "0.8.3"
knuffel = "3.2.0"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
regex = "1.11.3"
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
regex = "1.12.3"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+97
View File
@@ -1006,6 +1006,103 @@ where
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Blur {
pub off: bool,
pub passes: u8,
pub offset: f64,
pub noise: f64,
pub saturation: f64,
}
impl Default for Blur {
fn default() -> Self {
Self {
off: false,
passes: 3,
offset: 3.,
noise: 0.02,
saturation: 1.5,
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BlurPart {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub passes: Option<u8>,
#[knuffel(child, unwrap(argument))]
pub offset: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub noise: Option<FloatOrInt<0, 1000>>,
#[knuffel(child, unwrap(argument))]
pub saturation: Option<FloatOrInt<0, 1000>>,
}
impl MergeWith<BlurPart> for Blur {
fn merge_with(&mut self, part: &BlurPart) {
self.off |= part.off;
if part.on {
self.off = false;
}
merge_clone!((self, part), passes);
merge!((self, part), offset, noise, saturation);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffectRule {
#[knuffel(child, unwrap(argument))]
pub xray: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub blur: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub noise: Option<FloatOrInt<0, 1000>>,
#[knuffel(child, unwrap(argument))]
pub saturation: Option<FloatOrInt<0, 1000>>,
}
/// Resolved background effect rule.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffect {
/// Whether to render with xray effect (see through).
///
/// - `None`: xray if any background effect is active
/// - `Some(false)`: no xray
/// - `Some(true)`: xray even if no other background effect is active
pub xray: Option<bool>,
/// Whether to blur the background.
///
/// - `None`: blur when the window/layer requests it (e.g. through ext-background-effect
/// protocol)
/// - `Some(false)`: never blur
/// - `Some(true)`: always blur
pub blur: Option<bool>,
pub noise: Option<f64>,
pub saturation: Option<f64>,
}
impl MergeWith<BackgroundEffectRule> for BackgroundEffect {
fn merge_with(&mut self, part: &BackgroundEffectRule) {
merge_clone_opt!((self, part), xray, blur);
if let Some(x) = part.noise {
self.noise = Some(x.0);
}
if let Some(x) = part.saturation {
self.saturation = Some(x.0);
}
}
}
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
+35 -39
View File
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
@@ -132,6 +133,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 +141,7 @@ pub enum Action {
ScreenshotWindowById {
id: u64,
write_to_disk: bool,
show_pointer: bool,
path: Option<String>,
},
ToggleKeyboardShortcutsInhibit,
@@ -354,6 +357,8 @@ pub enum Action {
SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget,
#[knuffel(skip)]
StopCast(u64),
ToggleOverview,
OpenOverview,
CloseOverview,
@@ -364,7 +369,7 @@ pub enum Action {
#[knuffel(skip)]
UnsetWindowUrgent(u64),
#[knuffel(skip)]
LoadConfigFile,
LoadConfigFile(#[knuffel(argument)] Option<String>),
#[knuffel(skip)]
MruAdvance {
direction: MruDirection,
@@ -407,15 +412,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,13 +693,14 @@ 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,
niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
niri_ipc::Action::LoadConfigFile { path } => Self::LoadConfigFile(path),
}
}
}
@@ -761,7 +770,7 @@ where
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
let mut seen_keys = HashSet::new();
let mut seen_keys: HashMap<Key, &knuffel::ast::SpannedNode<S>> = HashMap::new();
let mut binds = Vec::new();
@@ -771,39 +780,26 @@ where
ctx.emit_error(e);
}
Ok(bind) => {
if seen_keys.insert(bind.key) {
binds.push(bind);
} else {
// ideally, this error should point to the previous instance of this keybind
//
// i (sodiboo) have tried to implement this in various ways:
// miette!(), #[derive(Diagnostic)]
// DecodeError::Custom, DecodeError::Conversion
// nothing seems to work, and i suspect it's not possible.
//
// DecodeError is fairly restrictive.
// even DecodeError::Custom just wraps a std::error::Error
// and this erases all rich information from miette. (why???)
//
// why does knuffel do this?
// from what i can tell, it doesn't even use DecodeError for much.
// it only ever converts them to a Report anyways!
// https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
//
// besides like, allowing downstream users (such as us!)
// to match on parse failure, i don't understand why
// it doesn't just use a generic error type
//
// even the matching isn't consistent,
// because errors can also be omitted as ctx.emit_error.
// why does *that one* especially, require a DecodeError?
//
// anyways if you can make it format nicely, definitely do fix this
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"keybind",
"duplicate keybind",
));
match seen_keys.entry(bind.key) {
Entry::Occupied(entry) => {
// Even though it's technically incorrect, we use
// `DecodeError::Missing` here because it labels the bind with
// "node starts here", which is the least bad option
ctx.emit_error(DecodeError::missing(
entry.get(),
"keybind first defined here",
));
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"keybind",
"duplicate keybind later defined here",
));
}
Entry::Vacant(entry) => {
entry.insert(child);
binds.push(bind);
}
}
}
}
@@ -1026,7 +1022,7 @@ impl FromStr for Key {
// [0]: https://github.com/xkbcommon/libxkbcommon/blob/45a118d5325b051343b4b174f60c1434196fa7d4/src/keysym.c#L276
// [1]: https://docs.rs/xkbcommon/latest/xkbcommon/xkb/keysyms/index.html#:~:text=KEY%5FXF86ScreenSaver
//
// See https://github.com/YaLTeR/niri/issues/1969
// See https://github.com/niri-wm/niri/issues/1969
if keysym == Keysym::XF86_Screensaver {
keysym = keysym_from_name(key, KEYSYM_NO_FLAGS);
if keysym.raw() == KEY_NoSymbol {
+4
View File
@@ -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,
+2
View File
@@ -364,6 +364,8 @@ pub struct Tablet {
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
#[knuffel(child)]
pub map_to_focused_output: bool,
#[knuffel(child)]
pub left_handed: bool,
}
+8 -1
View File
@@ -1,5 +1,6 @@
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
use crate::window_rule::PopupsRule;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
@@ -20,6 +21,10 @@ pub struct LayerRule {
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -28,4 +33,6 @@ pub struct Match {
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
#[knuffel(property, str)]
pub layer: Option<niri_ipc::Layer>,
}
+119 -8
View File
@@ -59,7 +59,9 @@ use crate::recent_windows::RecentWindowsPart;
pub use crate::recent_windows::{MruDirection, MruFilter, MruPreviews, MruScope, RecentWindows};
pub use crate::utils::FloatOrInt;
use crate::utils::{Flag, MergeWith as _};
pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule};
pub use crate::window_rule::{
FloatingPosition, PopupsRule, RelativeTo, ResolvedPopupsRules, WindowRule,
};
pub use crate::workspace::{Workspace, WorkspaceLayoutPart};
const RECURSION_LIMIT: u8 = 10;
@@ -78,6 +80,7 @@ pub struct Config {
pub hotkey_overlay: HotkeyOverlay,
pub config_notification: ConfigNotification,
pub animations: Animations,
pub blur: Blur,
pub gestures: Gestures,
pub overview: Overview,
pub environment: Environment,
@@ -194,6 +197,7 @@ where
"hotkey-overlay" => m_merge!(hotkey_overlay),
"config-notification" => m_merge!(config_notification),
"animations" => m_merge!(animations),
"blur" => m_merge!(blur),
"gestures" => m_merge!(gestures),
"overview" => m_merge!(overview),
"xwayland-satellite" => m_merge!(xwayland_satellite),
@@ -291,13 +295,71 @@ where
}
"include" => {
let path: PathBuf = utils::parse_arg_node("include", node, ctx)?;
let base = ctx.get::<BasePath>().unwrap();
let path = base.0.join(path);
// 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()),
));
}
// We use DecodeError::Missing throughout this block because it results in the
// least confusing error messages while still allowing to provide a span.
// Expand ~ into the home dir
let path = if let Ok(rest) = path.strip_prefix("~") {
let Some(home) = std::env::home_dir() else {
ctx.emit_error(DecodeError::missing(
node,
format!("error retrieving home directory to expand {path:?}"),
));
continue;
};
home.join(rest)
} else {
// Otherwise, use the current include base dir
let base = ctx.get::<BasePath>().unwrap();
base.0.join(path)
};
let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
if recursion == RECURSION_LIMIT {
ctx.emit_error(DecodeError::missing(
@@ -369,10 +431,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}"),
));
}
}
}
}
@@ -651,6 +719,7 @@ mod tests {
tablet {
map-to-output "eDP-1"
map-to-focused-output
calibration-matrix 1.0 2.0 3.0 \
4.0 5.0 6.0
}
@@ -1043,6 +1112,7 @@ mod tests {
map_to_output: Some(
"eDP-1",
),
map_to_focused_output: true,
left_handed: false,
},
touch: Touch {
@@ -1566,6 +1636,13 @@ mod tests {
},
),
},
blur: Blur {
off: false,
passes: 3,
offset: 3.0,
noise: 0.02,
saturation: 1.5,
},
gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll {
trigger_width: 10.0,
@@ -1795,6 +1872,22 @@ mod tests {
),
scroll_factor: None,
tiled_state: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
popups: PopupsRule {
opacity: None,
geometry_corner_radius: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
},
},
],
layer_rules: [
@@ -1809,6 +1902,7 @@ mod tests {
),
),
at_startup: None,
layer: None,
},
],
excludes: [],
@@ -1829,6 +1923,22 @@ mod tests {
geometry_corner_radius: None,
place_within_backdrop: None,
baba_is_float: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
popups: PopupsRule {
opacity: None,
geometry_corner_radius: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
},
},
],
binds: Binds(
@@ -2134,6 +2244,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",
),
+45 -2
View File
@@ -1,8 +1,11 @@
use niri_ipc::ColumnDisplay;
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
use crate::appearance::{
BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
TabIndicatorRule,
};
use crate::layout::DefaultPresetSize;
use crate::utils::RegexEq;
use crate::utils::{MergeWith, RegexEq};
use crate::FloatOrInt;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -72,6 +75,46 @@ pub struct WindowRule {
pub scroll_factor: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub tiled_state: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
/// Rules for popup surfaces.
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct PopupsRule {
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
}
/// Resolved popup-specific rules.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct ResolvedPopupsRules {
/// Extra opacity to draw popups with.
pub opacity: Option<f32>,
/// Corner radius to assume the popups have.
pub geometry_corner_radius: Option<CornerRadius>,
/// Background effect configuration for popups.
pub background_effect: BackgroundEffect,
}
impl MergeWith<PopupsRule> for ResolvedPopupsRules {
fn merge_with(&mut self, part: &PopupsRule) {
if let Some(x) = part.opacity {
self.opacity = Some(x);
}
if let Some(x) = part.geometry_corner_radius {
self.geometry_corner_radius = Some(x);
}
self.background_effect.merge_with(&part.background_effect);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "1.0.4", optional = true }
schemars = { version = "1.2.1", optional = true }
serde.workspace = true
serde_json.workspace = true
+2 -2
View File
@@ -1,6 +1,6 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
Types and helpers for interfacing with the [niri](https://github.com/niri-wm/niri) Wayland compositor.
## Backwards compatibility
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.11.0"
niri-ipc = "=26.4.0"
```
+134 -3
View File
@@ -41,7 +41,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.11.0"
//! niri-ipc = "=26.4.0"
//! ```
//!
//! ## Features
@@ -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.
@@ -915,7 +936,13 @@ pub enum Action {
///
/// Can be useful for scripts changing the config file, to avoid waiting the small duration for
/// niri's config file watcher to notice the changes.
LoadConfigFile {},
LoadConfigFile {
/// Path of a new config file to load.
///
/// If unset, reloads the current config file.
#[cfg_attr(feature = "clap", arg(long))]
path: Option<String>,
},
}
/// Change in window or column size.
@@ -1466,6 +1493,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 +1687,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 {
@@ -1751,6 +1868,20 @@ impl FromStr for Transform {
}
}
impl FromStr for Layer {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"background" => Ok(Self::Background),
"bottom" => Ok(Self::Bottom),
"top" => Ok(Self::Top),
"overlay" => Ok(Self::Overlay),
_ => Err("invalid layer, can be \"background\", \"bottom\", \"top\" or \"overlay\""),
}
}
}
impl FromStr for ModeToSet {
type Err = &'static str;
+37 -1
View File
@@ -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
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ 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"] }
niri = { version = "25.11.0", path = ".." }
niri-config = { version = "25.11.0", path = "../niri-config" }
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "26.4.0", path = ".." }
niri-config = { version = "26.4.0", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+2 -5
View File
@@ -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(
+10 -5
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use niri::animation::Clock;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode};
use niri::render_helpers::RenderTarget;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::{Color, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -268,12 +268,17 @@ 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();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
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(ctx, true, &mut |elem| rv.push(Box::new(elem) as _));
rv
}
}
+14 -4
View File
@@ -2,7 +2,8 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri::render_helpers::xray::XrayPos;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -119,9 +120,18 @@ impl TestCase for Tile {
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
);
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
let xray_pos = XrayPos::new(location, 1.);
self.tile
.render(renderer, location, true, RenderTarget::Output)
.map(|elem| Box::new(elem) as _)
.collect()
.render(ctx, location, xray_pos, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
}
}
+11 -11
View File
@@ -1,5 +1,5 @@
use niri::layout::{LayoutElement, SizingMode};
use niri::render_helpers::RenderTarget;
use niri::render_helpers::{RenderCtx, RenderTarget};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Scale, Size};
@@ -52,16 +52,16 @@ impl TestCase for Window {
.to_f64()
.downscale(2.);
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.window
.render(
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
.render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
}
}
+11 -2
View File
@@ -20,6 +20,7 @@ mod imp {
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
@@ -206,8 +207,15 @@ mod imp {
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
let cache = UserDataMap::new();
if element.is_framebuffer_effect() {
element
.capture_framebuffer(&mut frame, src, dst, &cache)
.context("error in capture_framebuffer()")?;
}
element
.draw(&mut frame, src, dst, &[damage], &[])
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
.context("error drawing element")?;
}
}
@@ -255,7 +263,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 {
+17 -24
View File
@@ -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::RenderCtx;
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::Kind;
@@ -149,36 +149,29 @@ impl LayoutElement for TestWindow {
false
}
fn render<R: NiriRenderer>(
fn render_normal<R: NiriRenderer>(
&self,
_renderer: &mut R,
_ctx: RenderCtx<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(
+4 -1
View File
@@ -60,7 +60,7 @@ SourceLicense: GPL-3.0-or-later
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib) AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
URL: https://github.com/niri-wm/niri
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
@@ -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
+18 -9
View File
@@ -1,11 +1,11 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://yalter.github.io/niri/Configuration:-Introduction
// https://niri-wm.github.io/niri/Configuration:-Introduction
// Input device configuration.
// Find the full list of options on the wiki:
// https://yalter.github.io/niri/Configuration:-Input
// https://niri-wm.github.io/niri/Configuration:-Input
input {
keyboard {
xkb {
@@ -73,7 +73,7 @@ input {
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Outputs
// https://niri-wm.github.io/niri/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
@@ -108,7 +108,7 @@ input {
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Layout
// https://niri-wm.github.io/niri/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 16
@@ -134,7 +134,7 @@ layout {
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
@@ -295,7 +295,7 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// Animation settings.
// The wiki explains how to configure individual animations:
// https://yalter.github.io/niri/Configuration:-Animations
// https://niri-wm.github.io/niri/Configuration:-Animations
animations {
// Uncomment to turn off all animations.
// off
@@ -306,7 +306,7 @@ animations {
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Window-Rules
// https://niri-wm.github.io/niri/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
@@ -550,14 +550,23 @@ binds {
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// Cycle through widths set in preset-column-widths.
Mod+R { switch-preset-column-width; }
// Cycling through the presets in reverse order is also possible.
// Mod+R { switch-preset-column-width-back; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Shift+R { switch-preset-column-width-back; }
Mod+Ctrl+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// 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; }
+7 -8
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
type = scripted
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
restart = false
+6
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -11,4 +11,4 @@ Before=xdg-desktop-autostart.target
[Service]
Slice=session.slice
Type=notify
ExecStart=/usr/bin/niri --session
ExecStart=niri --session
+248 -135
View File
@@ -67,7 +67,7 @@ use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
@@ -97,9 +97,6 @@ pub struct Tty {
dmabuf_global: Option<DmabufGlobal>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
// The ignored nodes have changed, but the session is paused, so we need to update it on
// resume.
update_ignored_nodes_on_resume: bool,
// Whether the debug tinting is enabled.
debug_tint: bool,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
@@ -434,12 +431,21 @@ 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)
}
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
// If the session is not active at startup (e.g. niri was launched from a different TTY),
// suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
// full re-enumeration of input devices instead of being a no-op.
if !session.is_active() {
debug!("session is not active, starting libinput in paused state");
libinput.suspend();
}
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
.insert_source(input_backend, |mut event, _, state| {
@@ -486,11 +492,6 @@ impl Tty {
}
info!("using as the render node: {node_path}");
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
warn!("ignoring the primary node or render node is not allowed");
}
Ok(Self {
config,
session,
@@ -499,17 +500,27 @@ impl Tty {
gpu_manager,
primary_node,
primary_render_node,
ignored_nodes,
ignored_nodes: HashSet::new(),
devices: HashMap::new(),
dmabuf_global: None,
update_output_config_on_resume: false,
update_ignored_nodes_on_resume: false,
debug_tint: false,
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn init(&mut self, niri: &mut Niri) {
// If the session is inactive, skip initialization because we won't be able to do much with
// the devices anyway. We'll get ActivateSession and add the devices there instead.
//
// This can happen when starting niri while having a different TTY active (e.g. via tmux).
if !self.session.is_active() {
return;
}
// Initialize the ignored nodes.
self.ignored_nodes = self.compute_ignored_nodes();
let udev = self.udev_dispatcher.clone();
let udev = udev.as_source_ref();
@@ -549,6 +560,10 @@ impl Tty {
return;
}
// Recompute ignored nodes to resolve symlinks (like /dev/dri/by-path/...) to their
// new underlying device IDs.
self.ignored_nodes = self.compute_ignored_nodes();
if let Err(err) = self.device_added(device_id, &path, niri) {
warn!("error adding device: {err:?}");
}
@@ -596,16 +611,9 @@ impl Tty {
warn!("error resuming libinput");
}
if self.update_ignored_nodes_on_resume {
self.update_ignored_nodes_on_resume = false;
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
if ignored_nodes.remove(&self.primary_node)
|| ignored_nodes.remove(&self.primary_render_node)
{
warn!("ignoring the primary node or render node is not allowed");
}
self.ignored_nodes = ignored_nodes;
}
// While the session was suspended, GPUs could have been added, so
// /dev/dri/by-path/... symlinks need to be re-resolved.
self.ignored_nodes = self.compute_ignored_nodes();
let mut device_list = self
.udev_dispatcher
@@ -646,7 +654,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/niri-wm/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 {
@@ -689,7 +706,14 @@ impl Tty {
}
// Add new devices.
for (device_id, path) in device_list.into_iter() {
//
// Add the primary node first as later nodes might depend on the primary render
// node being available.
let primary_device_id = self.primary_node.dev_id();
let primary_device_path = device_list.remove(&primary_device_id);
let primary = primary_device_path.map(|path| (primary_device_id, path));
for (device_id, path) in primary.into_iter().chain(device_list) {
if let Err(err) = self.device_added(device_id, &path, niri) {
warn!("error adding device: {err:?}");
}
@@ -799,7 +823,10 @@ impl Tty {
.context("error creating renderer")?;
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding wl-display in EGL: {err:?}");
// wl_drm is on its way out so this is expected on most modern distros.
trace!("error binding legacy EGL to wl_display: {err}");
} else {
debug!("bound legacy EGL to wl_display");
}
let gles_renderer = renderer.as_gles_renderer();
@@ -969,6 +996,32 @@ impl Tty {
} => {
removed.push(crtc);
}
// Emitted when the list of connector modes changes at runtime.
//
// Some devices, notably USB-C docks with DP-MST/alt-mode, report Connected before
// the EDID has been read, with an empty mode list. Then, at a later point, the
// modes will be populated, at which point we'll get this Changed event.
DrmScanEvent::Changed {
connector,
crtc: Some(crtc),
} => {
let connector_name = format_connector_name(&connector);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
debug!(
"connector changed: {} \"{}\"",
&name.connector,
name.format_make_model_serial(),
);
if !device.known_crtcs.contains_key(&crtc) {
// I guess this can happen if the connector initially wasn't mapped to a
// CRTC but then got mapped before being changed.
warn!("changed connector missing from known crtcs");
}
// We don't actually need to do anything here; on_output_config_changed() will
// take care of picking a new mode if needed.
}
_ => (),
}
}
@@ -1055,6 +1108,7 @@ impl Tty {
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting DrmCompositor state: {err:?}");
}
surface.compositor.reset_buffers();
}
}
@@ -1382,7 +1436,7 @@ impl Tty {
// Create the compositor.
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
OutputModeSource::Auto(output.downgrade()),
surface,
None,
device.allocator.clone(),
@@ -1412,7 +1466,7 @@ impl Tty {
.create_surface(crtc, mode, &[connector.handle()])?;
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
OutputModeSource::Auto(output.downgrade()),
surface,
None,
device.allocator.clone(),
@@ -1666,8 +1720,8 @@ impl Tty {
// This is an error!() because it shouldn't happen, but on some systems it somehow
// does. Kernel sending rogue vblank events?
//
// https://github.com/YaLTeR/niri/issues/556
// https://github.com/YaLTeR/niri/issues/615
// https://github.com/niri-wm/niri/issues/556
// https://github.com/niri-wm/niri/issues/615
error!(
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
can happen when resuming from sleep or powering on monitors: {state:?}"
@@ -1828,8 +1882,12 @@ impl Tty {
};
// Render the elements.
let mut elements =
niri.render::<TtyRenderer>(&mut renderer, output, true, RenderTarget::Output);
let ctx = RenderCtx {
renderer: &mut renderer,
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
@@ -2225,22 +2283,25 @@ impl Tty {
}
}
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_ignored_nodes_on_resume = true;
return;
}
fn compute_ignored_nodes(&self) -> HashSet<DrmNode> {
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
if ignored_nodes.remove(&self.primary_node)
|| ignored_nodes.remove(&self.primary_render_node)
{
warn!("ignoring the primary node or render node is not allowed");
}
ignored_nodes
}
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
// If we're inactive, we can't do anything, but we'll recompute in ActivateSession.
if !self.session.is_active() {
return;
}
let ignored_nodes = self.compute_ignored_nodes();
if ignored_nodes == self.ignored_nodes {
return;
}
@@ -3324,6 +3385,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 +3452,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 +3491,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,
),
}
"#);
}
}
+54 -9
View File
@@ -4,8 +4,10 @@ use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use anyhow::Context as _;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::egl::EGLDevice;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -14,13 +16,15 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::platform::wayland::WindowAttributesExtWayland;
use smithay::reexports::winit::window::Window;
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal};
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -28,6 +32,7 @@ pub struct Winit {
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
dmabuf_global: Option<DmabufGlobal>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -41,7 +46,8 @@ impl Winit {
let builder = Window::default_attributes()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
.with_title("niri")
.with_name("niri", "");
let (backend, winit) = winit::init_from_attributes(builder)?;
let output = Output::new(
@@ -135,6 +141,7 @@ impl Winit {
output,
backend,
damage_tracker,
dmabuf_global: None,
ipc_outputs,
})
}
@@ -142,7 +149,10 @@ impl Winit {
pub fn init(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
// wl_drm is on its way out so this is expected on most modern distros.
trace!("error binding legacy EGL to wl_display: {err}");
} else {
debug!("bound legacy EGL to wl_display");
}
resources::init(renderer);
@@ -162,9 +172,44 @@ impl Winit {
niri.update_shaders();
self.create_dmabuf_global(niri);
niri.add_output(self.output.clone(), None, false);
}
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
let default_feedback = || {
let display = renderer.egl_context().display();
let device =
EGLDevice::device_for_display(display).context("error getting EGL device")?;
let node = device
.try_get_render_node()
.context("error getting EGL device render node")?
.context("failed to query EGL device render node")?;
let primary_formats = renderer.dmabuf_formats();
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
.build()
.context("error building dmabuf feedback")
};
// Fallback to dmabuf v3 if we failed to build feedback.
let dmabuf_global = match default_feedback() {
Ok(feedback) => niri
.dmabuf_state
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
Err(err) => {
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
let primary_formats = renderer.dmabuf_formats();
niri.dmabuf_state
.create_global::<State>(&niri.display_handle, primary_formats)
}
};
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
}
pub fn seat_name(&self) -> String {
"winit".to_owned()
}
@@ -180,12 +225,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
let ctx = RenderCtx {
renderer: self.backend.renderer(),
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
+2
View File
@@ -107,6 +107,8 @@ pub enum Msg {
RequestError,
/// Print the overview state.
OverviewState,
/// List screencasts.
Casts,
}
#[derive(Clone, Debug, clap::ValueEnum)]
+18 -20
View File
@@ -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>,
-13
View File
@@ -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)");
}
}
+123
View File
@@ -0,0 +1,123 @@
use std::sync::{Arc, Mutex};
use smithay::delegate_background_effect;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Rectangle};
use smithay::wayland::background_effect::{
self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler,
};
use smithay::wayland::compositor::{
add_post_commit_hook, with_states, RegionAttributes, SurfaceData,
};
use crate::niri::State;
use crate::utils::region::region_to_non_overlapping_rects;
/// Per-surface cache for processed blur region (non-overlapping rects).
#[derive(Default)]
struct CachedBlurRegionUserData(Mutex<CachedBlurRegionInner>);
#[derive(Default)]
struct CachedBlurRegionInner {
/// Whether a region change is pending to be committed.
pending_dirty: bool,
/// Whether the region must be recomputed.
dirty: bool,
/// Whether the post-commit hook has been registered for this surface.
hook_registered: bool,
/// Cached non-overlapping rects in surface-local coordinates.
///
/// `None` means there's no blur region.
rects: Option<Arc<Vec<Rectangle<i32, Logical>>>>,
}
/// Gets the cached blur region for a surface, lazily recomputing if dirty.
pub fn get_cached_blur_region(states: &SurfaceData) -> Option<Arc<Vec<Rectangle<i32, Logical>>>> {
let cache = states
.data_map
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
let mut guard = cache.0.lock().unwrap();
if guard.dirty {
guard.dirty = false;
recompute_blur_region(states, &mut guard);
}
guard.rects.clone()
}
fn recompute_blur_region(states: &SurfaceData, inner: &mut CachedBlurRegionInner) {
let cached = &states.cached_state;
let rects = if let Some(arc) = &mut inner.rects {
if Arc::strong_count(arc) > 1 {
debug!("cloning rects due to non-unique reference");
}
arc
} else {
inner.rects.insert(Arc::new(Vec::new()))
};
let rects = Arc::make_mut(rects);
if cached.has::<BackgroundEffectSurfaceCachedState>() {
let mut guard = cached.get::<BackgroundEffectSurfaceCachedState>();
if let Some(region) = &guard.current().blur_region {
region_to_non_overlapping_rects(region, rects);
} else {
inner.rects = None;
}
return;
}
inner.rects = None;
}
fn mark_blur_region_pending_dirty(wl_surface: &WlSurface) {
let register_hook = with_states(wl_surface, |states| {
let cache = states
.data_map
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
let mut guard = cache.0.lock().unwrap();
guard.pending_dirty = true;
if guard.hook_registered {
false
} else {
guard.hook_registered = true;
true
}
});
if register_hook {
add_post_commit_hook::<State, _>(wl_surface, |_state, _dh, surface| {
with_states(surface, |states| {
if let Some(cache) = states.data_map.get::<CachedBlurRegionUserData>() {
let mut guard = cache.0.lock().unwrap();
if guard.pending_dirty {
guard.pending_dirty = false;
guard.dirty = true;
crate::render_helpers::background_effect::damage_surface(states);
}
} else {
error!("unexpected missing CachedBlurRegionUserData");
}
});
});
}
}
impl ExtBackgroundEffectHandler for State {
fn capabilities(&self) -> background_effect::Capability {
background_effect::Capability::Blur
}
fn set_blur_region(&mut self, wl_surface: WlSurface, _region: RegionAttributes) {
mark_blur_region_pending_dirty(&wl_surface);
}
fn unset_blur_region(&mut self, wl_surface: WlSurface) {
mark_blur_region_pending_dirty(&wl_surface);
}
}
delegate_background_effect!(State);
+28 -12
View File
@@ -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()) {
@@ -195,7 +195,10 @@ impl CompositorHandler for State {
// The mapped pre-commit hook deals with dma-bufs on its own.
self.remove_default_dmabuf_pre_commit_hook(surface);
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
let mapped = Mapped::new(window, rules, hook);
let mapped = {
let config = self.niri.config.borrow();
Mapped::new(window, rules, hook, &config)
};
let window = mapped.window.clone();
let target = if let Some(p) = &parent {
@@ -486,11 +489,10 @@ impl CompositorHandler for State {
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
self.store_unmap_snapshot(&window, output.as_ref());
}
}
@@ -498,7 +500,16 @@ impl CompositorHandler for State {
.root_surface
.retain(|k, v| k != surface && v != surface);
self.niri.dmabuf_pre_commit_hook.remove(surface);
// The object destruction order is not guaranteed to follow the logical role order. So for
// example when a client disconnects unexpectedly, WlSurface::destroyed() may be called
// before XdgShellHandler::toplevel_destroyed(). In this case, the surface will *not* have
// the default dmabuf pre-commit hook: it will still have the toplevel pre-commit hook.
//
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
// subsequent toplevel_destroyed() call.
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, &hook);
}
}
}
@@ -517,6 +528,11 @@ delegate_shm!(State);
impl State {
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if !surface.is_alive() {
error!("tried to add dmabuf pre-commit hook for a dead surface");
return;
}
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
@@ -556,13 +572,13 @@ impl State {
let s = surface.clone();
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
error!("tried to add dmabuf pre-commit hook when there was already one");
remove_pre_commit_hook(surface, prev);
remove_pre_commit_hook(surface, &prev);
}
}
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, hook);
remove_pre_commit_hook(surface, &hook);
} else {
error!("tried to remove dmabuf pre-commit hook but there was none");
}
+42 -6
View File
@@ -1,12 +1,11 @@
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::{get_parent, with_states};
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
use smithay::wayland::shell::wlr_layer::{
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, LayerSurfaceData,
WlrLayerShellHandler, WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
namespace: String,
) {
let output = if let Some(wl_output) = &wl_output {
Output::from_resource(wl_output)
self.niri.output_from_resource(wl_output)
} else {
self.niri.layout.active_output().cloned()
};
@@ -126,8 +125,10 @@ impl State {
let output_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
let hook = add_mapped_layer_pre_commit_hook(layer);
let mapped = MappedLayer::new(
layer.clone(),
hook,
rules,
output_size,
scale,
@@ -142,6 +143,21 @@ impl State {
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
} else {
// The surface remains mapped.
if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) {
// Check if the layer changed.
if mapped.take_recompute_rules_on_commit() {
let config = self.niri.config.borrow();
if mapped
.recompute_layer_rules(&config.layer_rules, self.niri.is_at_startup)
{
mapped.update_config(&config);
}
}
} else {
error!("MappedLayer missing for a mapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely
@@ -155,7 +171,7 @@ impl State {
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand surfaces in
// update_keyboard_focus(), so we don't need to check for that here.
//
// https://github.com/YaLTeR/niri/issues/641
// https://github.com/niri-wm/niri/issues/641
let on_demand = layer.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::OnDemand;
if was_unmapped && on_demand {
@@ -204,3 +220,23 @@ impl State {
true
}
}
fn add_mapped_layer_pre_commit_hook(layer: &LayerSurface) -> HookId {
add_pre_commit_hook::<State, _>(layer.wl_surface(), move |state, _dh, surface| {
let layer_changed = with_states(surface, |states| {
let mut guard = states.cached_state.get::<LayerSurfaceCachedState>();
let pending_layer = guard.pending().layer;
let current_layer = guard.current().layer;
pending_layer != current_layer
});
if layer_changed {
for mapped in state.niri.mapped_layer_surfaces.values_mut() {
if mapped.surface().wl_surface() == surface {
mapped.set_recompute_rules_on_commit();
break;
}
}
}
})
}
+69 -37
View File
@@ -1,3 +1,4 @@
pub mod background_effect;
mod compositor;
mod layer_shell;
mod xdg_shell;
@@ -13,16 +14,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 +42,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,
@@ -314,32 +314,60 @@ 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();
self.niri.on_maybe_dnd_ended();
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
// example. On successful drop, additionally activate the target window.
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,29 +377,29 @@ 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);
}
}
}
self.niri.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
trace!("dnd cancelled");
self.niri.on_maybe_dnd_ended();
}
}
impl ServerDndGrabHandler for State {}
impl crate::niri::Niri {
fn on_maybe_dnd_ended(&mut self) {
self.layout.dnd_end();
self.dnd_icon = None;
// FIXME: more granular
self.queue_redraw_all();
}
}
delegate_data_device!(State);
@@ -444,7 +472,7 @@ impl SessionLockHandler for State {
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
let Some(output) = Output::from_resource(&output) else {
let Some(output) = self.niri.output_from_resource(&output) else {
warn!("no Output matching WlOutput");
return;
};
@@ -529,7 +557,9 @@ impl ForeignToplevelHandler for State {
{
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if let Some(requested_output) =
wl_output.and_then(|o| self.niri.output_from_resource(&o))
{
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
@@ -605,14 +635,16 @@ delegate_ext_workspace!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
// This can happen if the output was removed before this was called.
if !self.niri.output_exists(screencopy.output()) {
trace!("screencopy output no longer exists");
return;
}
// If with_damage then push it onto the queue for redraw of the output,
// otherwise render it immediately.
if screencopy.with_damage() {
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
+54 -42
View File
@@ -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);
@@ -617,7 +612,7 @@ impl XdgShellHandler for State {
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
if let Some((mapped, current_output)) = self
.niri
@@ -851,9 +846,7 @@ impl XdgShellHandler for State {
self.niri
.stop_casts_for_target(CastTarget::Window { id: id.get() });
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
self.store_unmap_snapshot(&window, output.as_ref());
let transaction = Transaction::new();
let blocker = transaction.blocker();
@@ -868,7 +861,15 @@ impl XdgShellHandler for State {
self.niri.window_mru_ui.remove_window(id);
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
let surface = surface.wl_surface();
// This check is necessary because implicit resource destruction is done with
// undefined order, so the surface might get destroyed before toplevel_destroyed() is
// called. In this case, adding the default pre-commit hook here would leak it, since the
// place that removes it is WlSurface::destroyed(), which had already been called by now.
if surface.is_alive() {
self.add_default_dmabuf_pre_commit_hook(surface);
}
// If this is the only instance, then this transaction will complete immediately, so no
// need to set the timer.
@@ -1256,7 +1257,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 +1291,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) => {
@@ -1430,7 +1443,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
let Some((mapped, output)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
@@ -1466,7 +1479,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;
@@ -1532,9 +1545,8 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
state.store_unmap_snapshot(&window, output.as_ref());
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
+105 -38
View File
@@ -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,14 +32,17 @@ 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;
use self::move_grab::MoveGrab;
use self::pick_color_grab::PickColorGrab;
use self::pick_window_grab::PickWindowGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
#[cfg(feature = "dbus")]
@@ -49,7 +53,7 @@ use crate::niri::{CastTarget, PointerVisibility, State};
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;
@@ -291,6 +295,7 @@ impl State {
I::Device: 'static,
{
let device_output = event.device().output(self);
let device_output = device_output.filter(|output| self.niri.output_exists(output));
let device_output = device_output.as_ref();
let (target_geo, keep_ratio, px, transform) =
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
@@ -486,19 +491,17 @@ impl State {
}
}
if pressed
&& raw == Some(Keysym::Escape)
&& (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
{
// We window picking state so the pick window grab must be active.
// Unsetting it cancels window picking.
this.niri
.seat
.get_pointer()
.unwrap()
.unset_grab(this, serial, time);
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
if pressed && raw == Some(Keysym::Escape) {
// Cancel certain grabs on Escape.
let pointer = this.niri.seat.get_pointer().unwrap();
if pointer
.with_grab(|_, grab| Self::grab_can_be_cancelled_with_esc(grab))
.unwrap_or(false)
{
pointer.unset_grab(this, serial, time);
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
}
}
if let Some(Keysym::space) = raw {
@@ -741,7 +744,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 +753,7 @@ impl State {
output,
mapped,
write_to_disk,
show_pointer,
path,
) {
warn!("error taking screenshot: {err:?}");
@@ -760,6 +764,7 @@ impl State {
Action::ScreenshotWindowById {
id,
write_to_disk,
show_pointer,
path,
} => {
let mut windows = self.niri.layout.windows();
@@ -772,6 +777,7 @@ impl State {
output,
mapped,
write_to_disk,
show_pointer,
path,
) {
warn!("error taking screenshot: {err:?}");
@@ -2230,13 +2236,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();
@@ -2285,9 +2293,9 @@ impl State {
}
self.niri.queue_redraw_all();
}
Action::LoadConfigFile => {
Action::LoadConfigFile(path) => {
if let Some(watcher) = &self.niri.config_file_watcher {
watcher.load_config();
watcher.load_config(path);
}
}
Action::MruConfirm => {
@@ -2454,6 +2462,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 +2620,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 +2718,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 +2892,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 +3084,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
@@ -3212,8 +3261,8 @@ impl State {
let horizontal_amount = event.amount(Axis::Horizontal);
let vertical_amount = event.amount(Axis::Vertical);
// Handle touchpad scroll bindings.
if source == AxisSource::Finger {
// Handle touchpad and continuous scroll bindings.
if source == AxisSource::Finger || source == AxisSource::Continuous {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
@@ -3986,6 +4035,7 @@ impl State {
fallback_output: Option<&Output>,
) -> Option<Point<f64, Logical>> {
let output = evt.device().output(self);
let output = output.filter(|output| self.niri.output_exists(output));
let output = output.as_ref().or(fallback_output)?;
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let transform = output.current_transform();
@@ -4106,7 +4156,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 +4248,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 +4291,19 @@ 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>>()
}
fn grab_can_be_cancelled_with_esc(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
let grab = grab.as_any();
grab.is::<PickWindowGrab>() || grab.is::<PickColorGrab>() || Self::is_dnd_grab(grab)
}
}
/// Check whether the key should be intercepted and mark intercepted
@@ -4623,7 +4686,11 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_dwt_set_enabled(c.dwt);
let _ = device.config_dwtp_set_enabled(c.dwtp);
let _ = device.config_tap_set_drag_lock_enabled(c.drag_lock);
let _ = device.config_tap_set_drag_lock_enabled(if c.drag_lock {
input::DragLockState::EnabledTimeout
} else {
input::DragLockState::Disabled
});
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed.0);
let _ = device.config_left_handed_set(c.left_handed);
+75 -30
View File
@@ -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) {
+12 -7
View File
@@ -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, RenderCtx, RenderTarget};
pub struct PickColorGrab {
start_data: PointerGrabStartData<State>,
@@ -48,15 +49,15 @@ impl PickColorGrab {
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::<i32, Physical>::from((1, 1));
let elements = data.niri.render(
let ctx = RenderCtx {
renderer,
&output,
false,
// This is an interactive operation so we can render without blocking out.
RenderTarget::Output,
);
target: RenderTarget::Output,
xray: None,
};
let elements = data.niri.render_to_vec(ctx, &output, false);
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,
};
+84 -37
View File
@@ -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(
+71 -3
View File
@@ -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")?;
@@ -192,7 +193,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
return Ok(());
}
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
windows.sort_unstable_by_key(|a| a.id);
for window in windows {
print_window(&window);
@@ -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 {
+129 -1
View File
@@ -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)
@@ -458,7 +463,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
fn validate_action(action: &Action) -> Result<(), String> {
if let Action::Screenshot { path, .. }
| Action::ScreenshotScreen { path, .. }
| Action::ScreenshotWindow { path, .. } = action
| Action::ScreenshotWindow { path, .. }
| Action::LoadConfigFile { path } = action
{
if let Some(path) = path {
// Relative paths are resolved against the niri compositor's working directory, which
@@ -469,6 +475,13 @@ fn validate_action(action: &Action) -> Result<(), String> {
}
}
if let Action::LoadConfigFile { path: Some(path) } = action {
let p = Path::new(path);
if !p.is_file() {
return Err(format!("path does not point to a file: {path}"));
}
}
Ok(())
}
@@ -793,6 +806,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;
+150 -35
View File
@@ -1,21 +1,23 @@
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};
use smithay::desktop::{LayerSurface, PopupKind, PopupManager};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::compositor::{remove_pre_commit_hook, HookId};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
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::xray::XrayPos;
use crate::render_helpers::{background_effect, RenderCtx};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
@@ -23,15 +25,26 @@ pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,
/// Pre-commit hook that we have on all mapped layer surfaces.
pre_commit_hook: HookId,
/// Up-to-date rules.
rules: ResolvedLayerRules,
/// Whether to recompute layer rules on the next commit.
///
/// Set in the pre-commit hook when the layer changes; consumed in the commit handler.
recompute_rules_on_commit: bool,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: SolidColorBuffer,
/// The shadow around the surface.
shadow: Shadow,
/// The blur config, passed for background effect rendering.
blur_config: niri_config::Blur,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
@@ -47,12 +60,14 @@ niri_render_elements! {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
BackgroundEffect = BackgroundEffectElement,
}
}
impl MappedLayer {
pub fn new(
surface: LayerSurface,
pre_commit_hook: HookId,
rules: ResolvedLayerRules,
view_size: Size<f64, Logical>,
scale: f64,
@@ -66,11 +81,14 @@ impl MappedLayer {
Self {
surface,
pre_commit_hook,
rules,
recompute_rules_on_commit: false,
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
view_size,
scale,
shadow: Shadow::new(shadow_config),
blur_config: config.blur,
clock,
}
}
@@ -81,6 +99,8 @@ impl MappedLayer {
shadow_config.on = false;
shadow_config.merge_with(&self.rules.shadow);
self.shadow.update_config(shadow_config);
self.blur_config = config.blur;
}
pub fn update_shaders(&mut self) {
@@ -129,6 +149,14 @@ impl MappedLayer {
true
}
pub fn set_recompute_rules_on_commit(&mut self) {
self.recompute_rules_on_commit = true;
}
pub fn take_recompute_rules_on_commit(&mut self) -> bool {
std::mem::take(&mut self.recompute_rules_on_commit)
}
pub fn place_within_backdrop(&self) -> bool {
if !self.rules.place_within_backdrop {
return false;
@@ -156,19 +184,25 @@ impl MappedLayer {
Point::from((0., y))
}
pub fn render<R: NiriRenderer>(
pub fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
location: Point<f64, Logical>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
xray_pos: XrayPos,
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) {
let bob_offset = self.bob_offset();
let location = location + bob_offset;
let xray_pos = xray_pos.offset(bob_offset);
let surface = self.surface.wl_surface();
let should_block_out = ctx.target.should_block_out(self.rules.block_out_from);
if should_block_out {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
@@ -179,40 +213,121 @@ 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(
renderer,
push_elements_from_surface_tree(
ctx.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(ctx.renderer, location, &mut |elem| push(elem.into()));
rv
let geometry = Rectangle::new(location, self.block_out_buffer.size());
let surface_off = Point::new(0., 0.); // No geometry on layer surfaces.
let surface_anim_scale = Scale::from(1.);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
background_effect::render_for_tile(
ctx.as_gles(),
ns,
geometry,
self.scale,
false,
surface,
surface_off,
surface_anim_scale,
self.blur_config,
radius,
self.rules.background_effect,
should_block_out,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render_popups<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
) {
if ctx.target.should_block_out(self.rules.block_out_from) {
return;
}
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let bob_offset = self.bob_offset();
let location = location + bob_offset;
let xray_pos = xray_pos.offset(bob_offset);
let surface = self.surface.wl_surface();
for (popup, offset) in PopupManager::popups_for_surface(surface) {
let popup_rules = match popup {
PopupKind::Xdg(_) => self.rules.popups,
// IME popups aren't affected by rules for regular popups.
PopupKind::InputMethod(_) => niri_config::ResolvedPopupsRules::default(),
};
let alpha = alpha * popup_rules.opacity.unwrap_or(1.).clamp(0., 1.);
let surface = popup.wl_surface();
let popup_geo = popup.geometry();
let surface_loc = location + (offset - popup_geo.loc).to_f64();
push_elements_from_surface_tree(
ctx.renderer,
surface,
surface_loc.to_physical_precise_round(scale),
scale,
alpha,
Kind::ScanoutCandidate,
&mut |elem| push(elem.into()),
);
let geometry = Rectangle::new(location + offset.to_f64(), popup_geo.size.to_f64());
let surface_off = popup_geo.loc.upscale(-1).to_f64();
let surface_anim_scale = Scale::from(1.);
let mut effect = popup_rules.background_effect;
// Default xray to false for pop-ups since they're always on top of something.
if effect.xray.is_none() {
effect.xray = Some(false);
}
let xray_pos = xray_pos.offset(offset.to_f64());
background_effect::render_for_tile(
ctx.as_gles(),
ns,
geometry,
self.scale,
false,
surface,
surface_off,
surface_anim_scale,
self.blur_config,
popup_rules.geometry_corner_radius.unwrap_or_default(),
effect,
false,
xray_pos,
&mut |elem| push(elem.into()),
);
}
}
}
impl Drop for MappedLayer {
fn drop(&mut self) {
remove_pre_commit_hook(self.surface.wl_surface(), &self.pre_commit_hook);
}
}
+28 -23
View File
@@ -1,13 +1,14 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::utils::MergeWith as _;
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use niri_config::{BackgroundEffect, BlockOutFrom, CornerRadius, ResolvedPopupsRules, ShadowRule};
use smithay::desktop::LayerSurface;
use smithay::wayland::shell::wlr_layer::Layer;
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>,
@@ -26,33 +27,19 @@ pub struct ResolvedLayerRules {
/// Whether to bob this window up and down.
pub baba_is_float: bool,
/// Background effect configuration.
pub background_effect: BackgroundEffect,
/// Rules for this layer surface's popups.
pub popups: ResolvedPopupsRules,
}
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| {
@@ -90,6 +77,12 @@ impl ResolvedLayerRules {
}
resolved.shadow.merge_with(&rule.shadow);
resolved
.background_effect
.merge_with(&rule.background_effect);
resolved.popups.merge_with(&rule.popups);
}
resolved
@@ -103,5 +96,17 @@ fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
}
}
if let Some(layer) = m.layer {
let surface_layer = match surface.layer() {
Layer::Background => niri_ipc::Layer::Background,
Layer::Bottom => niri_ipc::Layer::Bottom,
Layer::Top => niri_ipc::Layer::Top,
Layer::Overlay => niri_ipc::Layer::Overlay,
};
if layer != surface_layer {
return false;
}
}
true
}
+34 -7
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::rc::Rc;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -20,7 +21,7 @@ use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::render_helpers::{render_to_encompassing_texture, RenderCtx, RenderTarget};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
@@ -28,6 +29,12 @@ pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer<GlesTexture>,
/// Contents that are not blocked out, but the background is blocked out.
///
/// If `None` then the background doesn't have any blocked-out surfaces, and normal `buffer`
/// can be used instead.
buffer_with_blocked_out_bg: Option<TextureBuffer<GlesTexture>>,
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer<GlesTexture>,
@@ -43,6 +50,9 @@ pub struct ClosingWindow {
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the texture with blocked-out bg should be offset.
buffer_with_blocked_out_bg_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
@@ -120,17 +130,27 @@ impl ClosingWindow {
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (buffer_with_blocked_out_bg, buffer_with_blocked_out_bg_offset) =
if let Some(contents) = snapshot.contents_with_blocked_out_bg {
let (buffer, offset) = render_to_texture(contents)
.context("error rendering contents with blocked-out bg")?;
(Some(buffer), offset)
} else {
(None, Point::default())
};
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
buffer_with_blocked_out_bg,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
buffer_with_blocked_out_bg_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
@@ -158,13 +178,17 @@ impl ClosingWindow {
pub fn render(
&self,
renderer: &mut GlesRenderer,
ctx: RenderCtx<GlesRenderer>,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
let (buffer, offset) = if ctx.target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else if ctx.target != RenderTarget::Output && self.buffer_with_blocked_out_bg.is_some() {
(
self.buffer_with_blocked_out_bg.as_ref().unwrap(),
self.buffer_with_blocked_out_bg_offset,
)
} else {
(&self.buffer, self.buffer_offset)
};
@@ -199,7 +223,10 @@ impl ClosingWindow {
let progress = anim.value();
let clamped_progress = anim.clamped_value().clamp(0., 1.);
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
if Shaders::get(ctx.renderer)
.program(ProgramType::Close)
.is_some()
{
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
@@ -229,14 +256,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,
)
+14 -15
View File
@@ -18,7 +18,8 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::TransactionBlocker;
use crate::utils::{
center_preferring_top_left_in_area, clamp_preferring_top_left_in_area, ensure_min_max_size,
@@ -489,6 +490,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
// Now, descendants is in back-to-front order, and repositioning them in the front-to-back
// order will preserve the subsequent indices and work out right.
let mut idx = idx;
#[allow(clippy::explicit_counter_loop)]
for descendant_idx in descendants.into_iter().rev() {
self.raise_window(descendant_idx, idx);
idx += 1;
@@ -1053,23 +1055,22 @@ impl<W: LayoutElement> FloatingSpace<W> {
true
}
pub fn render_elements<R: NiriRenderer>(
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
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.
//
// 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());
let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
let active = self.active_window_id.clone();
@@ -1077,13 +1078,11 @@ 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),
);
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
rv
}
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
+5 -9
View File
@@ -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 {
+3 -2
View File
@@ -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)
}
}
+155 -58
View File
@@ -59,12 +59,14 @@ use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::offscreen::OffscreenData;
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::xray::{Xray, XrayPos};
use crate::render_helpers::{BakedBuffer, RenderCtx};
use crate::rubber_band::RubberBand;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -112,6 +114,7 @@ niri_render_elements! {
LayoutElementRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -132,6 +135,11 @@ pub trait LayoutElement {
/// Unique ID of this element.
fn id(&self) -> &Self::Id;
/// Updates the config for the element.
fn update_config(&mut self, blur_config: niri_config::Blur) {
let _ = blur_config;
}
/// Visual size of the element.
///
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
@@ -154,35 +162,55 @@ pub trait LayoutElement {
/// location.
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>>;
xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
self.render_popups(ctx.r(), location, scale, alpha, xray_pos, push);
self.render_normal(ctx.r(), location, scale, alpha, push);
}
/// Renders the non-popup parts of the element.
fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
ctx: RenderCtx<R>,
location: Point<f64, Logical>,
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 _ = (ctx, location, scale, alpha, push);
}
/// Renders the popups of the element.
fn render_popups<R: NiriRenderer>(
&self,
renderer: &mut R,
ctx: RenderCtx<R>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
self.render(renderer, location, scale, alpha, target).popups
xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
let _ = (ctx, location, scale, alpha, xray_pos, push);
}
/// Renders the background effect behind the main surface of the element.
#[allow(clippy::too_many_arguments)]
fn render_background_effect(
&self,
_ctx: RenderCtx<GlesRenderer>,
_geometry: Rectangle<f64, Logical>,
_scale: f64,
_clip_to_geometry: bool,
_surface_anim_scale: Scale<f64>,
_radius: CornerRadius,
_xray_pos: XrayPos,
_push: &mut dyn FnMut(BackgroundEffectElement),
) {
}
/// Requests the element to change its size.
@@ -262,6 +290,9 @@ pub trait LayoutElement {
Some(requested)
}
fn is_windowed_fullscreen(&self) -> bool {
false
}
fn is_pending_windowed_fullscreen(&self) -> bool {
false
}
@@ -269,6 +300,22 @@ pub trait LayoutElement {
let _ = value;
}
/// The effective geometry corner radius for this element.
///
/// Returns zero when the element is in windowed fullscreen, since fullscreen windows have
/// square corners.
///
/// This method only handles windowed fullscreen and not maximized/real fullscreen. This is
/// because windowed fullscreen is handled by the element itself, whereas other sizing modes
/// are handled externally by the Tile, so the corner radius changes for those modes is also
/// handled externally.
fn geometry_corner_radius(&self) -> CornerRadius {
if self.is_windowed_fullscreen() {
return CornerRadius::default();
}
self.rules().geometry_corner_radius.unwrap_or_default()
}
fn is_child_of(&self, parent: &Self) -> bool;
fn rules(&self) -> &ResolvedWindowRules;
@@ -345,6 +392,7 @@ pub struct Options {
pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures,
pub overview: niri_config::Overview,
pub blur: niri_config::Blur,
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -498,6 +546,7 @@ pub enum HitType {
enum OverviewProgress {
Animation(Animation),
Gesture(OverviewGesture),
Open,
}
#[derive(Debug)]
@@ -604,6 +653,7 @@ impl Options {
animations: config.animations.clone(),
gestures: config.gestures,
overview: config.overview,
blur: config.blur,
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
deactivate_unfocused_windows: config.debug.deactivate_unfocused_windows,
@@ -628,6 +678,7 @@ impl OverviewProgress {
match self {
OverviewProgress::Animation(anim) => anim.value(),
OverviewProgress::Gesture(gesture) => gesture.value,
OverviewProgress::Open => 1.,
}
}
@@ -2648,9 +2699,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 +2727,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,10 +2773,20 @@ 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);
// We're not on any specific workspace so we can't compute a "workspace view" rect.
// Let's instead compute a rect relative to the output.
//
// FIXME: we could make the colors match up better in the overview by figuring out
// where a centered workspace would currently be, and computing the view rect
// against that. Since most of the time the dragged window will be on a centered
// workspace.
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
.downscale(zoom);
move_.tile.update_render_elements(true, view_rect);
}
}
@@ -2736,12 +2799,14 @@ impl<W: LayoutElement> Layout<W> {
..
} = &mut self.monitor_set
else {
error!("update_render_elements called with no monitors");
if output.is_some() {
error!("update_render_elements called with no monitors but Some output");
}
return;
};
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(_)));
@@ -2808,13 +2873,12 @@ impl<W: LayoutElement> Layout<W> {
ws.scrolling_insert_position(pos_within_workspace)
};
let rules = move_.tile.window().rules();
let border_width = move_.tile.effective_border_width().unwrap_or(0.);
let corner_radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
});
let corner_radius = move_
.tile
.window()
.geometry_corner_radius()
.expanded_by(border_width as f32);
mon.insert_hint = Some(InsertHint {
workspace: insert_ws,
position,
@@ -3271,7 +3335,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)
})
@@ -4586,12 +4650,33 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
window: &W::Id,
) {
let _span = tracy_client::span!("Layout::store_unmap_snapshot");
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
move_.tile.store_unmap_snapshot_if_empty(renderer);
let pos_within_output = move_.tile_render_location(zoom);
// Computation matches update_render_elements().
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
.downscale(zoom);
move_.tile.update_render_elements(false, view_rect);
move_.tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(pos_within_output, zoom),
);
return;
}
}
@@ -4599,9 +4684,15 @@ impl<W: LayoutElement> Layout<W> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for (ws, geo) in mon.workspaces_with_render_geo_mut(false) {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(geo.loc, zoom),
window,
);
return;
}
}
@@ -4610,7 +4701,13 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::default(),
window,
);
return;
}
}
@@ -4709,38 +4806,38 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn render_interactive_move_for_output<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
pub fn render_interactive_move_for_output<R: NiriRenderer>(
&self,
ctx: RenderCtx<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 pos_in_backdrop = move_.tile_render_location(zoom);
let xray_pos = XrayPos::new(pos_in_backdrop, zoom);
move_
.tile
.render(ctx, pos_in_backdrop, xray_pos, true, &mut |elem| {
push(RescaleRenderElement::from_element(
elem,
pos_in_backdrop.to_physical_precise_round(scale),
zoom,
));
});
}
pub fn refresh(&mut self, is_active: bool) {
+98 -112
View File
@@ -24,7 +24,8 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::SolidColorRenderElement;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
use crate::utils::{
@@ -282,6 +283,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 +872,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 +1491,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 +1646,35 @@ 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,
target: RenderTarget,
pub fn render_workspaces<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
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,79 @@ 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));
}
}
}};
}
let xray_pos = XrayPos::new(geo.loc, zoom);
ws.render_floating(ctx.r(), xray_pos, focus_ring, push!());
if let Some(loc) = insert_hint_render_loc {
if loc.workspace == InsertWorkspace::Existing(ws.id()) {
self.insert_hint_element
.render(ctx.renderer, loc.location, push!());
}
}
ws.render_scrolling(ctx.r(), xray_pos, 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) {
+3 -4
View File
@@ -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,
)
+58 -41
View File
@@ -21,7 +21,8 @@ use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge;
use crate::window::ResolvedWindowRules;
@@ -336,8 +337,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 +987,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 +1020,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 +1387,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 +1395,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 +1859,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 +2898,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,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
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());
let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
if self.columns.is_empty() {
return rv;
return;
}
let mut first = true;
@@ -2928,7 +2930,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(ctx.renderer, pos, &mut |elem| push(elem.into()));
}
for (tile, tile_off, visible) in col.tiles_in_render_order() {
@@ -2953,14 +2956,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
continue;
}
rv.extend(
tile.render(renderer, tile_pos, focus_ring, target)
.map(Into::into),
);
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
}
rv
}
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
@@ -3493,7 +3494,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 +4063,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() {
+7 -7
View File
@@ -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));
}
}
}
+7 -7
View File
@@ -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.
+69 -11
View File
@@ -116,10 +116,12 @@ impl TestWindow {
if self.0.animate_next_configure.get() {
self.0.animation_snapshot.replace(Some(RenderSnapshot {
contents: Vec::new(),
contents_with_blocked_out_bg: None,
blocked_out_contents: Vec::new(),
block_out_from: None,
size: self.0.bbox.get().size.to_f64(),
texture: OnceCell::new(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: OnceCell::new(),
}));
}
@@ -166,17 +168,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>,
@@ -252,6 +243,10 @@ impl LayoutElement for TestWindow {
self.0.requested_size.get()
}
fn is_windowed_fullscreen(&self) -> bool {
self.0.is_windowed_fullscreen.get()
}
fn is_pending_windowed_fullscreen(&self) -> bool {
self.0.is_pending_windowed_fullscreen.get()
}
@@ -3674,6 +3669,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;
+59
View File
@@ -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 = [
+281 -150
View File
@@ -18,6 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
@@ -27,7 +28,8 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{RenderCtx, RenderTarget};
use crate::utils::transaction::Transaction;
use crate::utils::{
baba_is_float_offset, round_logical_in_physical, round_logical_in_physical_max1,
@@ -130,6 +132,7 @@ niri_render_elements! {
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -248,6 +251,8 @@ impl<W: LayoutElement> Tile<W> {
let shadow_config = self.options.layout.shadow.merged_with(&rules.shadow);
self.shadow.update_config(shadow_config);
self.window.update_config(self.options.blur);
}
pub fn update_shaders(&mut self) {
@@ -398,12 +403,11 @@ impl<W: LayoutElement> Tile<W> {
self.shadow.update_config(shadow_config);
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
.unwrap_or_default()
let radius = self
.window
.geometry_corner_radius()
.fit_to(window_size.w as f32, window_size.h as f32);
self.rounded_corner_damage.set_corner_radius(radius);
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self) {
@@ -469,11 +473,22 @@ impl<W: LayoutElement> Tile<W> {
border_window_size.w -= border_width * 2.;
border_window_size.h -= border_width * 2.;
let radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
})
// FIXME: this takes into account the animation from normal sizing mode to
// maximized/fullscreen, but it doesn't take into account the corner radius animation from
// the window itself.
//
// Currently, an easy way to see the problem is to start from a window with a nonzero
// radius, then go from windowed fullscreen (that forces 0 radius) to regular fullscreen.
// At the start of the animation, windowed fullscreen becomes false, but the window hasn't
// animated to the normal fullscreen yet, so the radius here jumps to its nonzero value,
// even though it should remain zero throughout.
//
// Later, when windows get the surface shape protocol with radii, this issue will happen
// when that changes between animated commits.
let radius = self
.window
.geometry_corner_radius()
.expanded_by(border_width as f32)
.scaled_by(1. - expanded_progress as f32);
self.border.update_render_elements(
border_window_size,
@@ -492,9 +507,8 @@ impl<W: LayoutElement> Tile<W> {
let radius = if self.visual_border_width().is_some() {
radius
} else {
rules
.geometry_corner_radius
.unwrap_or_default()
self.window
.geometry_corner_radius()
.scaled_by(1. - expanded_progress as f32)
};
self.shadow.update_render_elements(
@@ -1007,13 +1021,14 @@ impl<W: LayoutElement> Tile<W> {
Point::from((0., y))
}
fn render_inner<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
fn render_inner<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
mut xray_pos: XrayPos,
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);
@@ -1038,65 +1053,69 @@ impl<W: LayoutElement> Tile<W> {
//
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
let bob_offset = self.bob_offset();
let location = location + bob_offset;
xray_pos = xray_pos.offset(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);
xray_pos = xray_pos.offset(window_loc);
let rules = self.window.rules();
// Clip to geometry including during the fullscreen animation to help with buggy clients
// that submit a full-sized buffer before acking the fullscreen state (Firefox).
let clip_to_geometry = fullscreen_progress < 1. && rules.clip_to_geometry == Some(true);
let radius = rules
.geometry_corner_radius
.unwrap_or_default()
let radius = self
.window
.geometry_corner_radius()
.scaled_by(1. - expanded_progress as f32);
// Popups go on top, whether it's resize or not.
self.window.render_popups(
ctx.r(),
window_render_loc,
scale,
win_alpha,
xray_pos,
&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(ctx.renderer) {
let mut ctx = ctx.as_gles();
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(
gles_renderer,
if let Some(texture_from) = resize.snapshot.texture(ctx.r(), scale) {
let mut window_elements = Vec::new();
self.window.render_normal(
ctx.r(),
Point::from((0., 0.)),
scale,
1.,
target,
&mut |elem| window_elements.push(elem),
);
let current = resize
.offscreen
.render(gles_renderer, scale, &window_elements)
.render(ctx.renderer, scale, &window_elements)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
let clip_to_geometry = if target
.should_block_out(resize.snapshot.block_out_from)
&& target.should_block_out(rules.block_out_from)
{
true
} else {
clip_to_geometry
};
let clip_to_geometry =
if ctx.target.should_block_out(resize.snapshot.block_out_from)
&& ctx.target.should_block_out(rules.block_out_from)
{
true
} else {
clip_to_geometry
};
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
@@ -1125,46 +1144,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);
let has_border_shader = BorderRenderElement::has_shader(ctx.renderer);
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_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned();
let clip = |elem| match elem {
LayoutElementRenderElement::Wayland(elem) => {
// If we should clip to geometry, render a clipped window.
if clip_to_geometry {
@@ -1213,37 +1219,41 @@ impl<W: LayoutElement> Tile<W> {
// Otherwise, render the solid color as is.
LayoutElementRenderElement::SolidColor(elem).into()
}
}));
elem @ LayoutElementRenderElement::BackgroundEffect(_) => {
// This is only used on popups for now. If subsurface blur is implemented, this
// will need to be handled somehow.
error!("background effect clipping is unimplemented");
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.render(geo);
push(damage.into());
}
self.window
.render_normal(ctx.r(), window_render_loc, scale, win_alpha, &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
// animated corner radius.
if fullscreen_progress < 1. && has_border_shader {
let border_width = self.visual_border_width().unwrap_or(0.);
let radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
})
let radius = self
.window
.geometry_corner_radius()
.expanded_by(border_width as f32)
.scaled_by(1. - expanded_progress as f32);
let size = self.fullscreen_backdrop.size();
let color = self.fullscreen_backdrop.color();
BorderRenderElement::new(
let elem = BorderRenderElement::new(
size,
Rectangle::from_size(size),
GradientInterpolation::default(),
@@ -1256,47 +1266,62 @@ 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(
ctx.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(ctx.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(ctx.renderer, location, &mut |elem| push(elem.into()));
}
let surface_anim_scale = animated_window_size / window_size;
self.window.render_background_effect(
ctx.as_gles(),
area,
self.scale,
clip_to_geometry,
surface_anim_scale,
radius,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
pub fn render<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
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,18 +1331,21 @@ 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 ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
&mut |elem| elements.push(elem),
);
match open.render(
renderer,
ctx.renderer,
&elements,
self.animated_tile_size(),
location,
@@ -1326,23 +1354,31 @@ 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:?}");
}
}
} 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<_>>>();
match alpha.offscreen.render(renderer, scale, &elements) {
let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
&mut |elem| elements.push(elem),
);
match alpha.offscreen.render(ctx.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,43 +1386,138 @@ 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(ctx, location, xray_pos, focus_ring, &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) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer));
self.unmap_snapshot =
Some(self.render_snapshot(renderer, xray, xray_has_blocked_out_layers, xray_pos));
}
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
mut xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
// 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(
renderer,
let mut contents = Vec::new();
self.render(
RenderCtx {
target: RenderTarget::Output,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
RenderTarget::Screencast,
&mut |elem| contents.push(elem),
);
let mut contents_with_blocked_out_bg = None;
// Do a bit of pointer surgery on Xray.
//
// The idea is to avoid the combinatorial combination of rendering snapshots for target
// (Output, Screencast) × Xray target (Output, Screencast, ScreenCapture).
//
// Our main goals:
// - Everything must look unblocked for RenderTarget::Output.
// - If anything is potentially blocked-out, it must not show up on any screen capture.
//
// Right above we rendered a fully-unblocked snapshot for the Output, so that's covered.
//
// Next, *only if Xray has any blocked-out surfaces* (which is a rare case), we will render
// a snapshot where the window itself is unblocked, but the Xray background is blocked. To
// do this, we swap the Output target buffers in Xray with the Screencast target buffers
// (which were prepared for us higher up the stack).
//
// Finally, we render a fully blocked-out snapshot. If Xray has blocked-out surfaces, then
// Xray's Screencast buffers are already filled-in, but if not, then we swap in the Output
// buffers, to avoid an extra render. This is safe since we know there are no blocked
// surfaces there.
let output_idx = RenderTarget::Output as usize;
let screencast_idx = RenderTarget::Screencast as usize;
let mut screencast_background = None;
let mut screencast_backdrop = None;
let mut output_background = None;
let mut output_backdrop = None;
if let Some(xray) = &mut xray {
screencast_background = Some(Rc::clone(&xray.background[screencast_idx]));
screencast_backdrop = Some(Rc::clone(&xray.backdrop[screencast_idx]));
output_background = Some(Rc::clone(&xray.background[output_idx]));
output_backdrop = Some(Rc::clone(&xray.backdrop[output_idx]));
if xray_has_blocked_out_layers {
xray.background[output_idx] = screencast_background.clone().unwrap();
xray.backdrop[output_idx] = screencast_backdrop.clone().unwrap();
let mut contents = Vec::new();
self.render(
RenderCtx {
target: RenderTarget::Output,
renderer,
xray: Some(xray),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| contents.push(elem),
);
contents_with_blocked_out_bg = Some(contents);
} else {
xray.background[screencast_idx] = output_background.clone().unwrap();
xray.backdrop[screencast_idx] = output_backdrop.clone().unwrap();
}
}
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let mut blocked_out_contents = Vec::new();
self.render(
RenderCtx {
target: RenderTarget::Screencast,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| blocked_out_contents.push(elem),
);
// Put everything back to normal.
if let Some(xray) = &mut xray {
if xray_has_blocked_out_layers {
xray.background[output_idx] = output_background.take().unwrap();
xray.backdrop[output_idx] = output_backdrop.take().unwrap();
} else {
xray.background[screencast_idx] = screencast_background.take().unwrap();
xray.backdrop[screencast_idx] = screencast_backdrop.take().unwrap();
}
}
RenderSnapshot {
contents: contents.collect(),
blocked_out_contents: blocked_out_contents.collect(),
contents,
contents_with_blocked_out_bg,
blocked_out_contents,
block_out_from: self.window.rules().block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: Default::default(),
}
}
+46 -26
View File
@@ -32,7 +32,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;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::RenderCtx;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -1386,7 +1387,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 +1625,45 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn render_elements<R: NiriRenderer>(
pub fn render_scrolling<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
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(ctx, xray_pos, scrolling_focus_ring, &mut |elem| {
push(elem.into())
});
}
pub fn render_floating<R: NiriRenderer>(
&self,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
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(ctx, xray_pos, view_rect, 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 {
@@ -1680,14 +1687,27 @@ impl<W: LayoutElement> Workspace<W> {
) || !self.render_above_top_layer()
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
window: &W::Id,
) {
let view_size = self.view_size();
for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::new(view_pos, view_size);
tile.update_render_elements(false, view_rect);
tile.store_unmap_snapshot_if_empty(renderer);
let xray_pos = xray_pos.offset(tile_pos);
tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
xray_pos,
);
return;
}
}
+2 -8
View File
@@ -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;

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