Compare commits

...

324 Commits

Author SHA1 Message Date
Ivan Molodetskikh 9cb2fe4eeb wip extra overview scale 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh cc2549323d wip 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 43c1592ab7 layout/tab_indicator: Use round_max1 where appropriate 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 78fe4a68db layout/monitor: Extract workspaces_render_geo() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 4fe2722a28 Simplify condition 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 7da5fc6169 Extract is_layout_obscured_under() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 3d4b762bcc Put the top layer above bottom and background layer popups
Makes it consistent with how window popups are below the top layer, also
will make more sense for the overview.
2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 74b016202b Add missing bounds checks to move-workspace actions
Fixes panics.
2025-04-25 10:54:09 +03:00
Ivan Molodetskikh 6ab055a4b9 niri.spec.rpkg: Recommend waybar
Now it's spawned by the default config.
2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 98bd9b7abb niri.spec.rpkg: Fix License 2025-04-22 22:51:56 +03:00
Ivan Molodetskikh f36e1c2ef2 default-config: Spawn waybar at startup
Make it a bit less of an empty screen.
2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 2243615fe9 default-config: Set titles for the default-bound apps 2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 7884d3bfea layout: Extract Monitor::update_shaders() 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh fdbc485d78 layout: Remove width and is_full_width from InsertHint
They were unused.
2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 7e253d2687 layout: Don't pass scale to render unnecessarily
These parts of the layout already know their scale.
2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 15ba2ab300 Rename render_floating_for_output to render_interactive_move_for_output 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 37840a418a animation: Extract value_at() and fix animations off difference 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 4a4c972ffb animation: Add more getters 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh ba933773ab animation: Fix restarted() Spring using old from/to 2025-04-16 07:46:10 +03:00
Ivan Molodetskikh f1cca1a6ca Back out "chore: update smithay"
This backs out commit 763cd564e3.

There are graphical glitches and a panic.
2025-04-16 07:46:10 +03:00
Simonas Kazlauskas 763cd564e3 chore: update smithay 2025-04-15 13:01:24 -07:00
Ivan Molodetskikh 95eafba346 README: Add link to RustCon talk 2025-04-12 19:38:57 +03:00
Ivan Molodetskikh df94662435 layout: Take into account idle time between last gesture event and end
Fixes cases like: do a quick movement with mouse, then hold it in-place for a
while (no events generated), then release the gesture (it uses all that
built-up speed). This also happens with DnD scroll and makes it go further than
intended.
2025-04-10 10:49:35 +03:00
Ivan Molodetskikh 430b155929 Fix typo in comment 2025-04-06 10:04:40 +03:00
Ivan Molodetskikh c359d24825 layout: Avoid calling interactive_move_end() in the middle of interactive_move_update() 2025-04-05 09:42:38 +03:00
Ivan Molodetskikh e8da89a430 input: Fix move-workspace-to-index being one off 2025-04-04 16:51:09 +03:00
Ivan Molodetskikh feae8c15e6 input: Don't panic on resize edge None when window is Some
This can already happen with the tab indicator, it will happen more onwards.
2025-04-04 16:04:18 +03:00
Ivan Molodetskikh b49f7dcb4d layout/scrolling: Use slice::fill()
Fix new Clippy warning.
2025-04-03 19:25:56 +03:00
Ivan Molodetskikh 60034a57ef wiki: Document baba-is-float 2025-04-01 10:35:17 +03:00
Ivan Molodetskikh 2adbf33fb6 Update issue template and contact links 2025-04-01 08:55:14 +03:00
Ivan Molodetskikh 28cc84fbd1 wiki: Remove excessive links 2025-03-31 14:35:20 +03:00
Ivan Molodetskikh e10b968eb0 layout: Reset unfullscreen view offset when starting interactive resize 2025-03-31 14:33:02 +03:00
LunarEclipse 3b1bf34e21 Allow negative shadow spread 2025-03-31 14:13:20 +03:00
LunarEclipse bd927b54e0 Improved layout shadow documentation 2025-03-31 13:47:47 +03:00
Ivan Molodetskikh 66d3a3bd82 Fix ToggleKeyboardShortcutsInhibit comment 2025-03-31 13:34:49 +03:00
sodiboo 36489f1daa add toggle-keyboard-shortcuts-inhibit to CLI/IPC (#1366)
* add toggle-keyboard-shortcuts-inhibit to CLI/IPC

missed it in ef8d5274b8
or https://github.com/YaLTeR/niri/pull/630
or 0584dd2f1e
or whatever

* Update niri-ipc/src/lib.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-31 05:00:10 +00:00
peelz b2c34e7fe9 Fix typo in comment 2025-03-29 07:56:47 -07:00
peelz dcc291d701 wiki: fix typo natuilus -> nautilus 2025-03-29 07:56:47 -07:00
lualeet 8d43efe4ac Add option 'focus-at-startup' to focus a chosen output on start (#1323)
* Implement default-output

* Fix incorrect wiki string

* Center mouse on start

* Move default-output to Output.focus-at-startup

* fixes

---------

Co-authored-by: lualeet <lualeet@null.null>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-29 10:13:59 +00:00
Ivan Molodetskikh 3bb7e60311 layout: Remove duplicated function 2025-03-29 12:50:16 +03:00
Ivan Molodetskikh d639eb0032 wiki: Document Steam black screen workaround 2025-03-29 11:26:31 +03:00
Ivan Molodetskikh d91499486e Make move-window-to-workspace focus=false work across monitors too 2025-03-29 11:17:38 +03:00
Ivan Molodetskikh f7106f9658 Update dependencies 2025-03-29 10:56:30 +03:00
Ivan Molodetskikh 835490c59a Update Smithay (protocol sanity checks) 2025-03-29 10:55:41 +03:00
dependabot[bot] 5cde00f6c6 build(deps): bump the rust-dependencies group across 1 directory with 2 updates
Bumps the rust-dependencies group with 2 updates in the / directory: [clap](https://github.com/clap-rs/clap) and [log](https://github.com/rust-lang/log).


Updates `clap` from 4.5.32 to 4.5.34
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.32...clap_complete-v4.5.34)

Updates `log` from 0.4.26 to 0.4.27
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.26...0.4.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-29 00:46:51 -07:00
nyx 7dc015e16b screenshot: make selection area modifiable via move/resize keybinds (#1279)
* screenshot: make selection area modifiable via keybinds

* input: run fmt

* Reimplement screenshot UI binds in a better way

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-29 07:40:21 +00:00
nyx 0db48e2f1b Add focus argument to move-window-to-workspace (#1332)
* layout: add focus flag to move-window-to-workspace

* lib: update comment

* misc: minor dup refactor

* input: format code

* layout: minor nit

* layout: update comment

* input: remove unnecessary conditionals

* misc: replace boolean

* tests: fix the failing one

* layout: change to smart

* ipc: Option<bool> -> bool

* lib: format code

* Rewrite focus doc comment

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-29 06:40:08 +00:00
Gavin Zhao 7cfecf4b1b wiki: Mention file chooser dependency and settings for portals (#1344)
* wiki: Mention file chooser dependency and settings for portals

* Update wiki/Important-Software.md

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-26 12:37:00 -07:00
Ivan Chernov 3142838e9e wiki: Documented flags for Electron based applications (#1302)
* wiki: Documented flags for Electron based applications

* Update wiki/Application-Issues.md

Co-authored-by: Kent Daleng <lolexplode@gmail.com>

* wiki: remove Arch specific files for Electron

* Apply suggestions from code review

---------

Co-authored-by: Kent Daleng <lolexplode@gmail.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-26 20:56:24 +03:00
Maya Nordland 4534d37266 wiki: Document window rule for steam notifications (#1341)
* wiki: Document window rule for steam notifications

* Update wiki/Application-Issues.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-25 07:39:59 +03:00
sodiboo ec5112d779 nix: update flake inputs and use new libgbm
fixes #1312
like
https://github.com/sodiboo/niri-flake/commit/0d54ea3f208f785b29f8396996b6bc8596d11a45
2025-03-23 12:21:55 -07:00
Ivan Molodetskikh c709696237 Don't block things out for pick-color
It's interactive so it's fine.
2025-03-23 11:45:54 +03:00
Ivan Molodetskikh b271409509 dbus/gnome_shell_screenshot: Fix pick_color return type 2025-03-23 09:31:51 +03:00
Ivan Molodetskikh 500dcca9b7 input: Suppress release from Pick grab clicks
Otherwise, it would trigger something inside the window.
2025-03-22 23:14:51 -07:00
nnyyxxxx 7210045b2a feat: support color picker functionality
chore: format code

refactor: improve quality

feat: implement gnomes PickColor method

refactor: minor code extraction

misc: fix reviews

fixes
2025-03-22 23:14:51 -07:00
Ivan Molodetskikh ed20822ce9 layout: Reset unfullscreen view offset when removing window
Another old bug found by randomized tests after I expanded the testing mock
window.
2025-03-22 13:57:37 +03:00
Jon Heinritz e88dfae46f main: Log to stderr instead of stdout
Currently we can't use logging in paths like niri msg that have meaningful
stdout. Logging to stderr makes that possible. Even if we don't want to log
anything in niri msg code paths, it's easy to have something accidentally log.
2025-03-22 12:29:11 +03:00
dependabot[bot] f95d5a82df build(deps): bump the rust-dependencies group across 1 directory with 2 updates
Bumps the rust-dependencies group with 2 updates in the / directory: [clap_complete](https://github.com/clap-rs/clap) and [glam](https://github.com/bitshifter/glam-rs).


Updates `clap_complete` from 4.5.46 to 4.5.47
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.46...clap_complete-v4.5.47)

Updates `glam` from 0.30.0 to 0.30.1
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.0...0.30.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-22 01:13:47 -07:00
Florian Finkernagel 7f72c358d5 Add option to warp-mouse-to-focus to always center 2025-03-22 01:00:43 -07:00
Ivan Molodetskikh 0d4f0f00c0 wiki: Document mod-key, mod-key-nested 2025-03-22 00:03:33 -07:00
peelz f2663c738c hotkey_overlay: rename ISO_Level{3,5}_Shift to Mod{5,3} 2025-03-22 00:03:33 -07:00
peelz c3609efb7a Add mod-key and mod-key-nested settings 2025-03-22 00:03:33 -07:00
Florian Finkernagel fd1f43673c Workspace-doc: reference set-workspace-name 2025-03-21 00:37:37 -07:00
Ivan Molodetskikh 9d10def7e8 wiki: Add Workspaces page 2025-03-21 09:26:21 +03:00
Ivan Molodetskikh e251ca7340 wiki: Document windowed fullscreen 2025-03-18 08:43:20 +03:00
Ivan Molodetskikh 9a527cc571 Track uncommitted windowed fullscreen state
Alright, this is the proper implementation. No more flickering.
2025-03-17 22:31:19 -07:00
Ivan Molodetskikh 39f52b7585 Implement toggle-windowed-fullscreen
Windowed, or fake, or detached, fullscreen, is when a window thinks that it's
fullscreen, but the compositor treats it as a normal window.
2025-03-17 22:31:19 -07:00
Ivan Molodetskikh b447b1f4de layout: Rename argument from window to id 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh 1a0fab05b6 layout: Don't forget to call on_commit() for the interactively moved window 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh fbb399f01d layout/tests: Implement going into fullscreen state 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh 6a80ec4704 layout/tile: Don't take fullscreen into account in min/max size
They are used strictly for non-fullscreen size computation.
2025-03-17 22:31:19 -07:00
Ivan Molodetskikh e8b158641b layout: Verify moved tile invariants 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh 27a715aded layout: Test that interactively moved window is not pending fullscreen 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh 926e63a5f3 Refactor request_fullscreen() to be an argument on request_size() 2025-03-17 22:31:19 -07:00
Ivan Molodetskikh e879199880 layout: Switch two places to workspaces_mut() 2025-03-17 22:31:19 -07:00
Cole Leavitt 5b6b6a5fe1 Add wait-for-frame-completion-in-pipewire debug flag for NVIDIA screencasts 2025-03-17 12:03:43 -07:00
Johannes Horner e11af089aa nix: Install shell completions (#1280) 2025-03-17 17:16:39 +03:00
Kent Daleng 5e549e1323 ci/wiki: check that (local) links are well formed (#1282)
* add check-links step, fix some links

* don't depend on build right now

* fix fragment

* reintroduce dependency for build

* don't only check links on push to main

* maybe this is a more sensible dependency tree for this stuff

* change commented suggestions, try v2.0.2 for action

* describe why we're on v2.0.2

* revert to %E2%80%90 (works with lychee anyway)
2025-03-16 20:15:37 +03:00
Ivan Molodetskikh 287480b541 Keep buffer size when switching dynamic cast to Nothing
Otherwise, we won't actually clear it because it'll become Pending.
2025-03-16 08:32:45 +03:00
Hilmar Wiegand a022fedd51 nix: Update flake.lock 2025-03-15 11:44:51 -07:00
Ivan Molodetskikh a4b8e100c0 Update Smithay & deps (fix panic during monitor hotplugging) 2025-03-15 20:42:22 +03:00
Ivan Molodetskikh 62576796be wiki: Add a Screencasting page 2025-03-15 19:58:18 +03:00
Ivan Molodetskikh 31891e6642 Implement dynamic screencast target 2025-03-15 09:55:46 -07:00
Kent Daleng 392fc27de1 Use anchors on the wiki (#1266)
* wiki testing

* wiki updates

* use .md with anchors, revert sidebar

* bump wiki action

* add some more anchors, fix some language

* change links to be more descriptive by themselves
2025-03-15 15:42:05 +00:00
Ivan Molodetskikh 9e560e7e60 Move CastTarget to src/niri.rs 2025-03-15 11:22:30 +03:00
Ivan Molodetskikh cee2ec7ab7 Use windows() instead of with_windows() 2025-03-15 11:18:54 +03:00
Ivan Molodetskikh 8c4ebb00a1 Store cast Stream ID, use it for Redraw request
Unlike StopCast, Redraw targets a specific Cast. Use the stream ID to
identify it.
2025-03-15 10:23:00 +03:00
Duncan Overbruck f6aa8c1793 Add move-column-to-index action 2025-03-14 12:57:33 -07:00
Duncan Overbruck a5d58d670b Add focus-column (by index) action 2025-03-14 12:57:33 -07:00
dependabot[bot] b4922086ce build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `a503d98` to `796c41c`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/a503d981d1be9a54b286ab5e160e4b9edddb500f...796c41c0294ccc195b0f59228d6467cc6c8f5de8)

Updates `smithay-drm-extras` from `a503d98` to `796c41c`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/a503d981d1be9a54b286ab5e160e4b9edddb500f...796c41c0294ccc195b0f59228d6467cc6c8f5de8)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 01:39:00 -07:00
Ivan Molodetskikh fd3b1f2b6c layout: Preserve previous view offset on consume-left 2025-03-14 08:50:29 +03:00
Ivan Molodetskikh ee0e2c7f1b Try default when configured xkb keymap fails to compile
Fixes panic at startup.
2025-03-13 21:39:07 +03:00
Ivan Molodetskikh 4f16be9e4d Wait for lock surfaces before locking
Fixes the red flash when locking.
2025-03-13 19:09:32 +03:00
Ivan Molodetskikh 0f30306fe5 Extract utils::is_mapped() 2025-03-13 18:56:35 +03:00
Ivan Molodetskikh 1c6037e612 Add tiled-state window rule, update the tiled state live 2025-03-13 14:14:54 +03:00
dbeley fed86fdb5d feat(trackpoint): document left-handed option 2025-03-13 03:36:20 -07:00
dbeley 3e21585861 feat(trackpoint): add left-handed option support 2025-03-13 03:36:20 -07:00
dependabot[bot] 9f9c4a99af build(deps): bump libc in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [libc](https://github.com/rust-lang/libc).


Updates `libc` from 0.2.170 to 0.2.171
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.171/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.170...0.2.171)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-12 06:34:54 -07:00
dependabot[bot] b220cdbe7e build(deps): bump the smithay group with 2 updates (#1243)
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `3219a0f` to `a503d98`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/3219a0f02a30de359f460ab165682a51cb13b7a5...a503d981d1be9a54b286ab5e160e4b9edddb500f)

Updates `smithay-drm-extras` from `3219a0f` to `a503d98`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/3219a0f02a30de359f460ab165682a51cb13b7a5...a503d981d1be9a54b286ab5e160e4b9edddb500f)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 12:08:11 +03:00
dependabot[bot] df219b5134 build(deps): bump the rust-dependencies group with 2 updates (#1242)
Bumps the rust-dependencies group with 2 updates: [clap](https://github.com/clap-rs/clap) and [clap_complete](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.31 to 4.5.32
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.5.31...clap_complete-v4.5.32)

Updates `clap_complete` from 4.5.45 to 4.5.46
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.45...clap_complete-v4.5.46)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 12:07:29 +03:00
dependabot[bot] 8cdabe8adf build(deps): bump serde in the rust-dependencies group (#1239)
Bumps the rust-dependencies group with 1 update: [serde](https://github.com/serde-rs/serde).


Updates `serde` from 1.0.218 to 1.0.219
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 09:22:26 +03:00
Annika Hannig 8737067af5 added move window to monitor by id 2025-03-10 23:17:36 -07:00
Annika Hannig 50a99f6356 Implemented move-window-to-monitor and move-column-to-monitor 2025-03-10 23:17:36 -07:00
Annika Hannig 993c5ce8af Implement focus-monitor to focus a specific monitor by output. 2025-03-10 23:17:36 -07:00
Toby Bridle 47dd338340 feat: 🎉 add show-pointer for Screenshot and ScreenshotScreen 2025-03-10 22:31:50 -07:00
Jon Heinritz 87b6c12625 Add Shell completions (#1226)
* feat(cli): add subcommand to generate shell completions

* Update src/cli.rs

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-10 20:14:34 +00:00
Ivan Molodetskikh b351f6ff22 Keep track of RenderElementStates in offscreens
This both avoids sending frame callbacks to surfaces invisible on the offscreen
(fixing Firefox with subsurface compositing in the process), and fixes
searching for split popups during the resize animation.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 12817a682d Store offscreen element id on Mapped instead of user data
We don't need user data for this.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 88614c08fe Make interactively moved window semitransparent 2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 4f5c8e745b Offscreen semitransparent tiles
Now that offscreen does damage tracking, we can reasonably do this. Note this
only affects full-tile opacity, not window opacity.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh f30413a744 layout/tile: Use animated tile size for open anim geo
This is the right value to use as the texture will also match the animated
size.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 3b8ce12316 tile: Use OffscreenBuffer for resize anims
OffscreenBuffer knows how to avoid recreating the texture every frame.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 880386e563 render_helpers/resize: Fix logic to allow for partially-filled texture
"texture geo" defines offset and src size, rather than the full texture size.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 266c6c3878 offscreen: Don't recreate if size decreased 2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 7b033aa7c6 offscreen: Track and return damage
This is the second part of the damage equation: now the offscreen element
itself reports correct damage, so partial redraws to the texture don't cause
full redraws of the texture element itself.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh efd8372b20 offscreen: Take damage into account when rendering
Does not yet signal the damage outside, but does skip rerendering if there was
no damage.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 74a30be10b Cache texture in OpenAnimation
Don't recreate it unless the size changes. This lays the groundwork for also
tracking damage in the future.
2025-03-10 07:59:14 +03:00
Ivan Molodetskikh 1c521e4831 Update Smithay (Framebuffer type) 2025-03-10 07:59:14 +03:00
Jon Heinritz eda43b2b93 doc: fix wrongly formatted link that rustdoc kept complaining about 2025-03-09 04:24:24 -07:00
LunarEclipse 593241d2f0 Added missing "Since: 25.02" to clipboard config documentation 2025-03-08 21:27:08 -08:00
Ivan Molodetskikh 69627bdc64 wiki: Document toggle-keyboard-shortcuts-inhibit and allow-inhibiting 2025-03-08 21:47:37 +03:00
Ivan Molodetskikh 3fa373c720 wiki: Add since to toggle-window-rule-opacity 2025-03-08 21:47:37 +03:00
Ivan Molodetskikh 083a56c729 wiki: Update the wording on the configuration breaking change policy 2025-03-08 14:54:34 +03:00
Ivan Molodetskikh 88fcf0c2a9 README: Mention tabs in features 2025-03-06 14:36:36 +03:00
dependabot[bot] 26618f8d50 build(deps): bump the rust-dependencies group with 5 updates
Bumps the rust-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.96` | `1.0.97` |
| [bitflags](https://github.com/bitflags/bitflags) | `2.8.0` | `2.9.0` |
| [bytemuck](https://github.com/Lokathor/bytemuck) | `1.21.0` | `1.22.0` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.139` | `1.0.140` |
| [insta](https://github.com/mitsuhiko/insta) | `1.42.1` | `1.42.2` |


Updates `anyhow` from 1.0.96 to 1.0.97
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.96...1.0.97)

Updates `bitflags` from 2.8.0 to 2.9.0
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.8.0...2.9.0)

Updates `bytemuck` from 1.21.0 to 1.22.0
- [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md)
- [Commits](https://github.com/Lokathor/bytemuck/compare/v1.21.0...v1.22.0)

Updates `serde_json` from 1.0.139 to 1.0.140
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.139...v1.0.140)

Updates `insta` from 1.42.1 to 1.42.2
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.42.1...1.42.2)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: bytemuck
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 03:36:44 -08:00
Ivan Molodetskikh 9f205d465c mapped: Omit popups from animation snapshot
It's used only for resizes, and those render popups on top.
2025-03-02 10:01:52 +03:00
Alex David d6e736aaf0 Allow disabling tap-and-drag (#1107)
* Allow disabling tap-and-drag

Similar to https://github.com/YaLTeR/niri/pull/1088, this adds a new
touchpad `drag` configuration option that configures tap-and-drag
behavior.

Currently tap-and-drag is always enabled when the `tap` setting is
enabled, but other compositors allow setting this separately.

* Update wiki/Configuration:-Input.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-02 10:01:34 +03:00
Martino Ferrari 36b28d9b96 Added top, left, bottom and right floating windows alignement (#1169)
* feat: added top, left, bottom, right alignement options

* feat: implemented extra alignement

* feat: added example

* doc: updated documentation with extra alignements

* doc: moved example in wiki and typo correction

* fix: relative position should be positive and not negative

* fixes

---------

Co-authored-by: Martino Ferrari <martinogiordano.ferrari@iter.org>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-03-01 17:46:27 +00:00
Ivan Molodetskikh 66113d7d76 wiki/FAQ: Remove mention of Waybar popups
This had been fixed a while ago.
2025-02-28 16:39:06 +03:00
dependabot[bot] aa2e8b402c build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `c24b431` to `25cf3cf`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/c24b431cc44458172f31276ca0fbade1ae09bdd9...25cf3cf41c5027ffeb55f90c683736726a81d71f)

Updates `smithay-drm-extras` from `c24b431` to `25cf3cf`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/c24b431cc44458172f31276ca0fbade1ae09bdd9...25cf3cf41c5027ffeb55f90c683736726a81d71f)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 00:45:33 -08:00
lanastara 311f3be5d8 wiki: remove wezterm issues (#1182)
* wiki: added note about nightly wezterm

* Update wiki/Application-Issues.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-02-27 19:16:27 +00:00
Ivan Molodetskikh 70dcd229cf Extract encompassing_geo() 2025-02-27 10:38:36 +03:00
Ivan Molodetskikh 26fe4a489a render_helpers: Use upscale(-1) 2025-02-27 09:54:26 +03:00
Ivan Molodetskikh 2363cf48e7 layout/monitor: Remove unused function 2025-02-27 08:21:05 +03:00
Ivan Molodetskikh 848294c09b layout/monitor: Remove redundant passthrough functions 2025-02-27 08:21:05 +03:00
Ivan Molodetskikh 693d935538 Add honor-xdg-activation-with-invalid-serial debug flag 2025-02-26 19:33:58 +03:00
bbb651 🇮🇱 16405b9b2b Implement niri msg pick-window
* feat: `niri msg pick-window`

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-02-26 15:22:27 +03:00
Ivan Molodetskikh 4719cc6d59 cursor: Extract get_render_cursor_named() 2025-02-26 15:10:18 +03:00
Ivan Molodetskikh 98b92d4db7 wiki: Add off to touch {} 2025-02-26 14:33:16 +03:00
nnyyxxxx 1bdded7a44 feat(input): add off option to touch device 2025-02-26 03:32:21 -08:00
dependabot[bot] 9bfe90dee1 build(deps): bump the rust-dependencies group across 1 directory with 3 updates
Bumps the rust-dependencies group with 3 updates in the / directory: [clap](https://github.com/clap-rs/clap), [portable-atomic](https://github.com/taiki-e/portable-atomic) and [schemars](https://github.com/GREsau/schemars).


Updates `clap` from 4.5.30 to 4.5.31
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.30...v4.5.31)

Updates `portable-atomic` from 1.10.0 to 1.11.0
- [Release notes](https://github.com/taiki-e/portable-atomic/releases)
- [Changelog](https://github.com/taiki-e/portable-atomic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/portable-atomic/compare/v1.10.0...v1.11.0)

Updates `schemars` from 0.8.21 to 0.8.22
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.21...v0.8.22)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: portable-atomic
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: schemars
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 02:43:00 -08:00
dependabot[bot] c153349c62 build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `0cd3345` to `c24b431`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/0cd3345c59f7cb139521f267956a1a4e33248393...c24b431cc44458172f31276ca0fbade1ae09bdd9)

Updates `smithay-drm-extras` from `0cd3345` to `c24b431`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/0cd3345c59f7cb139521f267956a1a4e33248393...c24b431cc44458172f31276ca0fbade1ae09bdd9)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 01:33:51 -08:00
Ivan Molodetskikh 5b6b5536fd Also check pointer for activation token validity
This actually doesn't matter in most cases currently, because it more or less
checks for *anything* to have a keyboard focus, so if you have some focused
window while clicking on a mako notification, that already qualifies.

Signed-off-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-02-24 22:34:30 +03:00
dependabot[bot] bac22dfe9f build(deps): bump the rust-dependencies group across 1 directory with 2 updates
Bumps the rust-dependencies group with 2 updates in the / directory: [libc](https://github.com/rust-lang/libc) and [log](https://github.com/rust-lang/log).


Updates `libc` from 0.2.169 to 0.2.170
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.170/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.169...0.2.170)

Updates `log` from 0.4.25 to 0.4.26
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.25...0.4.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 02:33:11 -08:00
Ivan Molodetskikh bca6545288 wiki: Add gestures to the list in configuration overview 2025-02-22 22:12:28 +03:00
Ivan Molodetskikh b94a5db879 Bump version to 25.02 2025-02-21 09:05:26 +03:00
Ivan Molodetskikh 4a4dcb85ef Update dependencies 2025-02-21 08:03:48 +03:00
Ivan Molodetskikh 7b70cb66bc wiki: Add Since to xkb file 2025-02-20 22:26:40 +03:00
Ivan Molodetskikh cd6522bcc6 wiki: Mention screenshow-screen/window write-to-disk=false 2025-02-20 22:26:40 +03:00
dependabot[bot] 8885233c7e build(deps): bump the rust-dependencies group with 4 updates
Bumps the rust-dependencies group with 4 updates: [anyhow](https://github.com/dtolnay/anyhow), [glam](https://github.com/bitshifter/glam-rs), [serde](https://github.com/serde-rs/serde) and [serde_json](https://github.com/serde-rs/json).


Updates `anyhow` from 1.0.95 to 1.0.96
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.95...1.0.96)

Updates `glam` from 0.29.2 to 0.30.0
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.29.2...0.30.0)

Updates `serde` from 1.0.217 to 1.0.218
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.217...v1.0.218)

Updates `serde_json` from 1.0.138 to 1.0.139
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.138...v1.0.139)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 01:08:29 -08:00
Ivan Molodetskikh 7478784343 Change default DnD scroll delay-ms to 100 2025-02-19 07:49:29 +03:00
Ivan Molodetskikh dca187de37 Don't snap after DnD scroll if view position didn't change
Otherwise, any DnD breaks temporarily centered columns.
2025-02-18 19:06:40 +03:00
Ivan Molodetskikh fe660a253b Don't activate window when pressing the Mod+MMB view gesture
Avoid unnecessary movement.
2025-02-18 19:06:40 +03:00
dependabot[bot] ad49e5820a build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.29 to 4.5.30
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.29...clap_complete-v4.5.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 01:50:42 -08:00
dependabot[bot] 4c40e6ce06 build(deps): bump ordered-float from 4.6.0 to 5.0.0
Bumps [ordered-float](https://github.com/reem/rust-ordered-float) from 4.6.0 to 5.0.0.
- [Release notes](https://github.com/reem/rust-ordered-float/releases)
- [Commits](https://github.com/reem/rust-ordered-float/compare/v4.6.0...v5.0.0)

---
updated-dependencies:
- dependency-name: ordered-float
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 01:50:32 -08:00
Ivan Molodetskikh 44c9797844 Take tab indicators into account in expand-column-to-available-width 2025-02-17 22:36:30 +03:00
Ivan Molodetskikh 652d2923bb Use toggle_full_width() for expand-column-to-available-width edge case 2025-02-17 21:57:35 +03:00
Ivan Molodetskikh 85349ce475 Fix expand-column-to-available-width for always-center 2025-02-17 21:48:00 +03:00
Ivan Molodetskikh 92cc2b89f7 Implement expand-column-to-available-width 2025-02-17 21:30:23 +03:00
dependabot[bot] 078383ea82 build(deps): bump pango in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [pango](https://github.com/gtk-rs/gtk-rs-core).


Updates `pango` from 0.20.7 to 0.20.9
- [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.20.7...0.20.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 02:18:38 -08:00
Ivan Molodetskikh d27d6a504d Make idle notify lazy 2025-02-17 09:09:59 +03:00
Ivan Molodetskikh ec5144feca Make pointer inactivity timer reset lazy 2025-02-17 09:04:07 +03:00
David 05e0e44a77 Fix link in Application-Issues.md 2025-02-16 09:16:59 -08:00
Ivan Molodetskikh 108e88e211 Enable fancy miette errors for the main binary
Seems there's not much dependency/binary size impact now, compared to when I
first made the KDL config.
2025-02-16 19:37:37 +03:00
Ivan Molodetskikh a693f64c41 Add blank line 2025-02-16 10:26:59 +03:00
Ivan Molodetskikh 5c0468d469 wiki: Document the DnD edge view scroll gesture and config 2025-02-16 10:18:00 +03:00
Ivan Molodetskikh f2b1fc66f2 Make DnD edge view scroll configurable 2025-02-16 10:18:00 +03:00
Ivan Molodetskikh 22302bf224 config: Deindent the snapshot 2025-02-16 10:18:00 +03:00
Ivan Molodetskikh bb6663ebac config: Convert parse test to a snapshot test
Updating it by hand got really old tbh
2025-02-16 10:18:00 +03:00
Ivan Molodetskikh c6e98d5a96 Add a small delay to DnD view scrolling 2025-02-16 10:18:00 +03:00
Ivan Molodetskikh d077350ae4 layout: Remove unused method 2025-02-16 10:18:00 +03:00
w-jablonski f01c840ebe Slightly clearer wording in Tabs.md 2025-02-15 05:15:07 -08:00
Ivan Molodetskikh ca1500ae90 Implement scrolling the view during DnD
DnD is external to the layout, so we just inform it when one is ongoing.
2025-02-15 13:28:57 +03:00
Ivan Molodetskikh d7f3ca00c7 Implement scrolling the view during interactive move 2025-02-15 13:28:57 +03:00
Ivan Molodetskikh fd8140e091 Hook up are_transitions_ongoing() for floating and tiles
Don't spoil it
2025-02-15 13:28:57 +03:00
Ivan Molodetskikh d94fbe9895 layout: Check move output in are_animations_ongoing() 2025-02-15 13:28:57 +03:00
Ivan Molodetskikh 7816f20e6a Implement ext-data-control 2025-02-14 09:03:20 +03:00
Ivan Molodetskikh 0d3610416c Update Smithay (idle-notify 2) 2025-02-14 09:03:20 +03:00
Ivan Molodetskikh 377ad54016 wiki: Document calibration-matrix 2025-02-14 09:03:20 +03:00
Ivan Chinenov 9e794f358b feat: support for setting tablet calibration matrix; this allows for rotating tablet inputs (#1122)
* feat: support for setting tablet calibration matrix

* Change default matrix
2025-02-14 05:15:45 +00:00
dependabot[bot] 4e17cbb9ea build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.28 to 4.5.29
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.28...clap_complete-v4.5.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 21:05:26 -08:00
rustN00b 4c98b87486 Add missing period to doc comment 2025-02-13 10:39:25 +03:00
rustN00b 5b753be213 Add missing periods to action doc comments 2025-02-13 10:37:41 +03:00
Ivan Molodetskikh a605e7f622 Implement custom hotkey overlay titles 2025-02-13 10:30:33 +03:00
Ivan Molodetskikh 513488f6b8 hotkey overlay: Add pretty for space 2025-02-13 10:30:33 +03:00
Ivan Molodetskikh 43ea4a172a hotkey overlay: Put Ctrl and Shift before Alt
They are commonly written this way.
2025-02-13 10:30:33 +03:00
Ivan Molodetskikh d47b59879a animation/spring: Add a check for from = to in duration()
The overdamped code below was dividing by zero in this case and triggering a
panic.
2025-02-13 08:47:23 +03:00
Ivan Molodetskikh ef80bcc834 Parse the config on the file watcher thread
It takes a while, so let's not block the main thread.
2025-02-12 20:56:32 +03:00
Ivan Molodetskikh eb8bd3894a watcher: Allow running a processing function on the thread 2025-02-12 20:56:32 +03:00
Ivan Molodetskikh 7e552333a9 tab indicator: Add corner-radius setting 2025-02-12 07:59:46 +03:00
Ivan Molodetskikh 213eafa203 wiki: Add Since to drag-lock 2025-02-11 18:24:17 +03:00
Ivan Molodetskikh 7b18ff8870 input: Intercept Enter key when confirming the exit dialog 2025-02-11 13:22:11 +03:00
Ivan Molodetskikh 5246e2ff25 wiki: Add note that is-window-cast-target doesn't match monitor casts 2025-02-11 10:40:51 +03:00
Ivan Molodetskikh dde9214ae4 wiki: Move sentence to a better spot 2025-02-11 10:35:15 +03:00
Ivan Molodetskikh 29b7a41692 Implement is-window-cast-target window rule matcher 2025-02-11 10:31:12 +03:00
Ivan Molodetskikh 216753678a wiki: Add a page for tabs 2025-02-11 08:30:03 +03:00
dependabot[bot] b9e67f6565 build(deps): bump zbus in the rust-dependencies group across 1 directory
Bumps the rust-dependencies group with 1 update in the / directory: [zbus](https://github.com/dbus2/zbus).


Updates `zbus` from 5.3.1 to 5.5.0
- [Release notes](https://github.com/dbus2/zbus/releases)
- [Commits](https://github.com/dbus2/zbus/compare/zbus-5.3.1...zbus-5.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 21:04:23 -08:00
Mikołaj Lercher 3a481b5250 wiki: describe running games with gamescope (#1112)
* wiki: describe running games with gamescope

* Update wiki/Application-Issues.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-02-11 07:52:53 +03:00
Ivan Molodetskikh 20769b4c2f tab indicator: Animate opening 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 14ac2cff4c tab indicator: Dim colors when column is inactive 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh fde627d955 Implement MulAssign<f32> for Color 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 6942ecc13a Implement clicking on tab to switch 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 963ff14ed0 Store hit type in PointContents 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 96a3ded2ec scrolling: Extract tab_indicator_area() 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh a21196ec54 tab indicator: Extract tab_rects() 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 0b83d9932b tab indicator: Use full column height 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 6bd92ab926 tab indicator: Fix gradient area computation
The gradient area should be relative to each tab's geometry. In most cases
these geometries will all match, but not when some tabs have a different size,
for example when they have a fixed size.
2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 02eccf7762 layout: Fix/add animations around tabbed columns 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 89cf276779 tests: Mark window_opening/target_size as slow 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh bc701cd529 mapped: Force a frame callback on configure
Lets hidden windows respond to events like resizes immediately. This is mainly
relevant for tabbed columns.

This commit doesn't actually force sending the frame callbacks in case we don't
redraw. We'll see if this is a problem or not.
2025-02-10 07:29:33 -08:00
Ivan Molodetskikh bfd81fc290 Make send_frame() a function on Mapped
We'll add some extra logic there.
2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 0dd8e883b0 tab indicator: Add gaps-between-tabs 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh c31b58e2c9 tab indicator: Implement place-within-column setting 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh b163045757 wiki: Add hide-when-single-tab to default-column-display example 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 41e9ec1364 wiki: Document tab indicators 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 64544a5726 tab indicator: Add position setting 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh d7d5a7f8f6 tab indicator: Add hide-when-single-tab 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh a451f75917 Implement tab indicators 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 1515410012 Add default-column-display window rule 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 8f9e0d029c Add set-column-display action 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 90f24da631 Move ColumnDisplay to niri-ipc 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh df70140b36 Allow tabbed columns to go fullscreen 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh f90eb0cbe4 Implement tabbed column display mode 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 55e2ea0c3b layout: Extract tile.hit(), HitType::hit_tile() 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 1d883931b4 Account for border in contents_under()
Fixes pointer clicks going through window borders to a layer surface below,
also fixes window not getting activated in all cases from a border click.
2025-02-10 07:29:33 -08:00
Ivan Molodetskikh b65fad09d8 Remove unnecessary mut 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 09a559d3c9 layout: Fix variable names 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 9fc749f3d4 layout/tile: Rename variable 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh f836d1c28a layout/scrolling: Extract activate_idx() 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 4f05a74aa8 Add alpha parameter to shaders
Lets us add extra opacity.
2025-02-10 07:29:33 -08:00
Ivan Molodetskikh c30f522ef2 shader: Return real alpha from alpha() 2025-02-10 07:29:33 -08:00
Ivan Molodetskikh 397e704d64 layout/scrolling: Extract variable 2025-02-07 10:03:38 +03:00
Ivan Molodetskikh acc9d3e409 layout/scrolling: Extract variable 2025-02-07 10:03:38 +03:00
Ivan Molodetskikh 0c59fc304c layout/scrolling: Use early return in tiles_origin() 2025-02-07 10:03:38 +03:00
Ivan Molodetskikh abd7f1dce3 layout/scrolling: Extract two variables 2025-02-07 10:03:38 +03:00
Ivan Molodetskikh 1d87da00b7 layout/scrolling: Extract two variables 2025-02-07 10:02:25 +03:00
Ivan Molodetskikh 91515ac6dc layout/scrolling: Extract resolve_* as methods on Column 2025-02-07 10:02:25 +03:00
Ivan Molodetskikh 7ec771f7ec layout: Rename toplevel_bounds() to new_window_toplevel_bounds() 2025-02-07 09:26:43 +03:00
Ivan Molodetskikh a42a5ac696 layout: Remove redundant () 2025-02-07 08:03:39 +03:00
Ivan Molodetskikh b31c0359eb layout: Extract col variable 2025-02-06 10:30:03 +03:00
Ivan Molodetskikh 934e5a6033 layout: Preserve focused window in column when window above is closed
Might be the longest standing bug in niri?
2025-02-06 09:41:15 +03:00
peelz 690d635505 Initialize tracing_subscriber earlier 2025-02-05 18:06:46 +03:00
Ivan Molodetskikh a444efd0eb Add focus-window-in-column (by index) action 2025-02-05 17:25:57 +03:00
Ivan Molodetskikh c41f93a468 Add focus-window-top/bottom/down-or-top/up-or-bottom actions 2025-02-05 17:25:51 +03:00
Mathias Zhang 900da597e4 input: add touchpad drag-lock setting 2025-02-05 13:35:13 +03:00
Ivan Molodetskikh d320833f40 Update Smithay (text-input double input fix) 2025-02-05 12:54:25 +03:00
dependabot[bot] c384b2489f build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.27 to 4.5.28
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.27...clap_complete-v4.5.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-05 12:52:57 +03:00
Ivan Molodetskikh ddcac86d1d mapped: Add needs_configure flag
Allows to de-duplicate configures from requests that require one.
2025-02-05 09:36:58 +03:00
Ivan Molodetskikh 734e3a6d3c Fix find_window_and_output() returning None with no outputs
As far as I can tell, this would mess up a ton of the logic. Not sure
how anything worked with no outputs before?
2025-02-05 09:35:10 +03:00
Ivan Molodetskikh f18b1a7043 mapped: Document RequestSizeOnce 2025-02-05 08:41:40 +03:00
Ivan Molodetskikh 7d24ad23c2 layout/scrolling: Extract tiles_origin() 2025-02-04 10:42:44 +03:00
Ivan Molodetskikh 691bc064bb wiki: Fix copy-paste typo 2025-02-04 10:42:44 +03:00
dependabot[bot] 553b1ba852 build(deps): bump the rust-dependencies group with 3 updates
Bumps the rust-dependencies group with 3 updates: [wayland-backend](https://github.com/smithay/wayland-rs), [wayland-scanner](https://github.com/smithay/wayland-rs) and [wayland-client](https://github.com/smithay/wayland-rs).


Updates `wayland-backend` from 0.3.7 to 0.3.8
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/commits)

Updates `wayland-scanner` from 0.31.5 to 0.31.6
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/commits)

Updates `wayland-client` from 0.31.7 to 0.31.8
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/commits)

---
updated-dependencies:
- dependency-name: wayland-backend
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wayland-scanner
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wayland-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 13:42:47 +03:00
Ivan Molodetskikh d5592743cb Add impl From<Color> for Gradient 2025-02-02 09:55:40 +03:00
Jesse Hallett 019e75955d document interaction between hide-when-typing and wine wayland (#1076)
* document interaction between hide-when-typing and wine wayland

* Update wiki/Configuration:-Miscellaneous.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-02-02 04:53:16 +00:00
Ivan Molodetskikh 32ad545f84 layout: Extract max_tile_height 2025-02-01 13:05:07 +03:00
Ivan Molodetskikh 4eddcef1be layout: Inline variable 2025-02-01 13:05:07 +03:00
Ivan Molodetskikh 68776f1cee layout: Verify that individual tiles don't get sized taller than working area 2025-02-01 10:48:16 +03:00
Ivan Molodetskikh a0e2a15c60 Take border into account for fixed preset-column-width for tiled windows 2025-01-31 21:30:22 +03:00
Ivan Molodetskikh 88c6778771 Extract SizeChange::from(PresetSize) 2025-01-31 21:15:43 +03:00
Ivan Molodetskikh 73f6d3366e wiki: Remove foot mention
This issue has been fixed in foot.
2025-01-31 20:42:50 +03:00
Ivan Molodetskikh 48a4d5c8a3 Fix typo in comment 2025-01-31 19:24:42 +03:00
Ivan Molodetskikh 6f2f7fa259 layout: Update module comment 2025-01-31 18:05:09 +03:00
Ivan Molodetskikh 49ddf66c2f layout: Move tests to separate file
This way changing just the tests won't rebuild the main library.
2025-01-31 17:56:43 +03:00
fable a169e0335d adjust horizontal view movement gestures snap points for center-focused-column "on-overflow" (#1052)
* Adjust snap points for center-focused-column "on-overflow"

* fix outer gaps not being accounted for in is_overflowing
2025-01-30 17:17:16 +03:00
may e412a0fc6b add option to set xkb config from file (#1062)
* add option to set xkb config from file

* Apply suggestions from code review

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-30 13:50:05 +00:00
dependabot[bot] fb5fedbf24 build(deps): bump pipewire from 86df391 to fd3d8f7
Bumps pipewire from `86df391` to `fd3d8f7`.

---
updated-dependencies:
- dependency-name: pipewire
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-30 13:57:34 +03:00
bbb651 6b04b1e454 misc: Use helper function for restriced protocol filters
I looked at cosmic-comp as a sanity check and they do the same thing,
I ended up yoinking their function name because it reads better,
not sure about "unrestricted" vs "privileged".
2025-01-30 07:18:42 +03:00
bbb651 0c340ec5ea misc: Use CursorImageSurfaceData type alias
instead of `Mutex<CursorImageAttributes>`
2025-01-30 07:18:42 +03:00
bbb651 34679c75a4 misc: Fix typos
Using [`typos`](https://github.com/crate-ci/typos) cli
2025-01-30 07:18:42 +03:00
Ivan Molodetskikh 1d3820a064 layout: Do not update original output for named workspaces upon adding windows
The way named workspaces are generally used makes them more "attached" to their
original output.

For example, you have a two-monitor setup with named workspaces on both. When
you disconnect the monitor to go somewhere and work for a while, then return,
you probably want your named workspaces to return to where they were on your
second monitor.

This is in contrast to unnamed workspaces which are more transient and should
more easily follow you wherever you're working.
2025-01-29 13:56:26 +03:00
Ivan Molodetskikh 1c749f578c layout: Update workspace original output on moving even if same monitor
Moving is an explicit action that puts the workspace on a specific monitor. It
makes sense to update the original output even if the workspace already happens
to be on the target monitor.
2025-01-29 13:56:26 +03:00
Ivan Molodetskikh 3a887a6e49 wiki/named-workspaces: Mention un/set-workspace-name 2025-01-29 13:56:26 +03:00
dependabot[bot] beef2da628 build(deps): bump the rust-dependencies group across 1 directory with 2 updates
Bumps the rust-dependencies group with 2 updates in the / directory: [serde_json](https://github.com/serde-rs/json) and [insta](https://github.com/mitsuhiko/insta).


Updates `serde_json` from 1.0.137 to 1.0.138
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.137...v1.0.138)

Updates `insta` from 1.42.0 to 1.42.1
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.42.0...1.42.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-29 11:28:39 +03:00
Ivan Molodetskikh 9b4d73f13a spec: Don't set XDG_RUNTIME_DIR
It should once again no longer be necessary.
2025-01-27 08:34:12 +03:00
Ivan Molodetskikh 0226d9aec2 Don't create on-disk sockets in tests 2025-01-27 08:30:22 +03:00
Ivan Molodetskikh 902222675a Use Niri::insert_client() in tests 2025-01-27 08:16:09 +03:00
Ivan Molodetskikh ec43493522 Extract Niri::insert_client() 2025-01-27 08:06:33 +03:00
Evgeny Zemtsov baa0518912 Extend switch-layout action to accept layout index (#1045)
* Extend switch-layout action to accept layout index

* Update src/input/mod.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-26 19:09:01 +00:00
Ivan Molodetskikh d665079b84 CI: Don't forget to build randomized tests in release 2025-01-26 09:54:40 +03:00
Ivan Molodetskikh f0d935dee1 CI: Further reduce the number of proptest cases 2025-01-26 09:39:04 +03:00
Ivan Molodetskikh 314b82caa0 CI: Reduce number of proptest cases 2025-01-26 09:20:49 +03:00
Ivan Molodetskikh 8f79139b78 CI: Add a randomized tests job 2025-01-26 08:37:25 +03:00
Ivan Molodetskikh c5296b870a CI: Write out dependencies once at the top 2025-01-26 08:37:24 +03:00
Ivan Molodetskikh 78697d1cea Switch Smithay back to git
Release currently has an unfortunate merge that breaks IMEs.
2025-01-25 11:51:45 +03:00
Kirottu 852da5714a Add move-workspace-to-index and move-workspace-to-monitor actions (#1007)
* Added move-workspace-to-index and move-workspace-to-monitor IPC actions

* Added redraws to the workspace handling actions, fixed tests that panicked, fixed other mentioned problems.

* Fixed workspace focusing and handling numbered workspaces with `move-workspace-to-index`

* Fixed more inconsistencies with move-workspace-to-monitor

* Added back `self.workspace_switch = None`

* Reordered some workspace cleanup logic

* Fix formatting

* Add missing blank lines

* Fix moving workspace to same monitor and wrong current index updating

* Move function up and add fixme comment

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-25 08:49:51 +00:00
Ivan Molodetskikh 4f79303811 CI: Remove version string from msrv job
Required checks on GitHub need to be updated every time otherwise.
2025-01-25 10:54:07 +03:00
Ivan Molodetskikh f294d527e1 wiki: Add clipboard section 2025-01-25 10:52:43 +03:00
peelz 54a1cd5069 Add clipboard disable-primary setting 2025-01-25 10:36:36 +03:00
Ivan Molodetskikh 748d90b443 Update Smithay to a crates.io version
What a time to be alive
2025-01-24 08:42:11 +03:00
bbb651 128b01e049 Add scroll-factor window rule 2025-01-23 12:07:32 +03:00
Ivan Molodetskikh 788c9c6c54 Add find_root_shell_surface() that goes through popups 2025-01-23 12:07:32 +03:00
Ivan Molodetskikh a10705fb20 Add toggle-window-rule-opacity action 2025-01-23 11:13:55 +03:00
dependabot[bot] b01b8afa8c build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.26 to 4.5.27
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.26...clap_complete-v4.5.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-21 11:56:26 +03:00
Ivan Molodetskikh acd4cb51aa Implement shadows for layer surfaces 2025-01-21 11:31:30 +03:00
Ivan Molodetskikh 5ebcae997e wiki: Add missing property to window rules example 2025-01-21 11:31:30 +03:00
Ivan Molodetskikh 2511a98e8b Extract Niri::update_shaders() 2025-01-21 11:31:30 +03:00
Ivan Molodetskikh a7692d10c4 Add update_render_elements() to MappedLayer 2025-01-21 11:31:30 +03:00
Ivan Molodetskikh c892f04c96 tile: Rename update() to update_render_elements() 2025-01-21 11:31:30 +03:00
Ivan Molodetskikh 3aad5a39ea Fix two comments 2025-01-21 11:31:30 +03:00
dependabot[bot] 7f025da5b6 build(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [sd-notify](https://github.com/lnicola/sd-notify) and [serde_json](https://github.com/serde-rs/json).


Updates `sd-notify` from 0.4.4 to 0.4.5
- [Changelog](https://github.com/lnicola/sd-notify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/lnicola/sd-notify/compare/v0.4.4...v0.4.5)

Updates `serde_json` from 1.0.135 to 1.0.137
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.135...v1.0.137)

---
updated-dependencies:
- dependency-name: sd-notify
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 13:53:29 +03:00
dependabot[bot] 01285bdbbe build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `fe31867` to `953959e`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/fe31867e3afac2543c4016fb8ed99df3e11eb6da...953959e6069b3e14dba96fdaa46c65990c21d5c9)

Updates `smithay-drm-extras` from `fe31867` to `953959e`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/fe31867e3afac2543c4016fb8ed99df3e11eb6da...953959e6069b3e14dba96fdaa46c65990c21d5c9)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 13:51:41 +03:00
Ivan Molodetskikh 8182484572 Remove Vec from Shadow::render() 2025-01-18 17:43:58 +03:00
sodiboo 0584dd2f1e implement keyboard-shortcuts-inhibit and wlr-virtual-pointer (#630)
* stub keyboard-shortcuts-inhibit and virtual-pointer impls

* implement keyboard-shortcuts-inhibit

* implement virtual-pointer

* deal with supressed key release edge-case; add allow-inhibiting property

* add toggle-keyboard-shortcuts-inhibit bind

* add InputBackend extensions; use Device::output() for absolute pos events

* add a `State` parameter to the backend exts and better document future intent

* Add some tests for is_inhibiting_shortcuts

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-18 17:26:42 +03:00
Ivan Molodetskikh bd559a2660 Implement window shadows 2025-01-17 23:10:01 +03:00
dependabot[bot] b4add625b2 build(deps): bump sd-notify in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [sd-notify](https://github.com/lnicola/sd-notify).


Updates `sd-notify` from 0.4.3 to 0.4.4
- [Changelog](https://github.com/lnicola/sd-notify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/lnicola/sd-notify/compare/v0.4.3...v0.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 11:34:41 +03:00
Val Packett 890bbff007 dbus: DisplayConfig: implement apply_monitors_config
This enables gnome-control-center to apply display configuration
changes. Only temporarily, persistence is ignored currently.
2025-01-17 11:16:10 +03:00
Val Packett b853d5b124 dbus: DisplayConfig: report fractional scales as supported 2025-01-17 11:16:10 +03:00
Val Packett 693e0e09f7 dbus: DisplayConfig: report disabled monitors in get_current_state
This is required for gnome-control-center to be able to turn
monitors back on.
2025-01-17 11:16:10 +03:00
Val Packett d52356b131 dbus: DisplayConfig: add properties required by display settings panel 2025-01-17 11:16:10 +03:00
dependabot[bot] b11b995d03 build(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [bitflags](https://github.com/bitflags/bitflags) and [log](https://github.com/rust-lang/log).


Updates `bitflags` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.7.0...2.8.0)

Updates `log` from 0.4.22 to 0.4.25
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.22...0.4.25)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 12:51:04 +03:00
Ivan Molodetskikh 99ba295082 Remove obsolete comment 2025-01-15 15:18:11 +03:00
Ivan Molodetskikh 8c2b5957eb Rename FoIPosition to FloatingPosition 2025-01-15 14:29:35 +03:00
dependabot[bot] 4472164447 build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `2a0d430` to `fe31867`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/2a0d4307430dc478b0b2f278bc5dc56ec02aa5ca...fe31867e3afac2543c4016fb8ed99df3e11eb6da)

Updates `smithay-drm-extras` from `2a0d430` to `fe31867`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/2a0d4307430dc478b0b2f278bc5dc56ec02aa5ca...fe31867e3afac2543c4016fb8ed99df3e11eb6da)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-15 12:32:32 +03:00
Ivan Molodetskikh a3cbe3514b clipped_surface: Store complete uniforms in the struct
This mistake shall never happen again.
2025-01-14 21:25:17 +03:00
Ivan Molodetskikh efa7c862a4 Add missing clipped surface uniform 2025-01-14 21:19:05 +03:00
Gustav Sörnäs 0df7a085de add write-to-disk argument to screenshot actions 2025-01-14 13:39:52 +03:00
dependabot[bot] 6ae51f287c build(deps): bump the smithay group with 2 updates
Bumps the smithay group with 2 updates: [smithay](https://github.com/Smithay/smithay) and [smithay-drm-extras](https://github.com/Smithay/smithay).


Updates `smithay` from `e1a863b` to `2a0d430`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/e1a863b3ffc2d560007e3b89e5bbe9500c69221e...2a0d4307430dc478b0b2f278bc5dc56ec02aa5ca)

Updates `smithay-drm-extras` from `e1a863b` to `2a0d430`
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/e1a863b3ffc2d560007e3b89e5bbe9500c69221e...2a0d4307430dc478b0b2f278bc5dc56ec02aa5ca)

---
updated-dependencies:
- dependency-name: smithay
  dependency-type: direct:production
  dependency-group: smithay
- dependency-name: smithay-drm-extras
  dependency-type: direct:production
  dependency-group: smithay
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 11:35:00 +03:00
Erica Z 36076d5279 make niri-session POSIX compatible (#970)
* make niri-session POSIX compatible

* Update resources/niri-session

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-14 09:41:50 +03:00
dependabot[bot] 427c4e3982 build(deps): bump directories from 5.0.1 to 6.0.0
Bumps [directories](https://github.com/soc/directories-rs) from 5.0.1 to 6.0.0.
- [Commits](https://github.com/soc/directories-rs/commits)

---
updated-dependencies:
- dependency-name: directories
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 13:36:17 +03:00
dependabot[bot] 1632ce87a5 build(deps): bump zbus in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [zbus](https://github.com/dbus2/zbus).


Updates `zbus` from 5.2.0 to 5.3.0
- [Release notes](https://github.com/dbus2/zbus/releases)
- [Commits](https://github.com/dbus2/zbus/compare/zbus-5.2.0...zbus-5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 13:35:10 +03:00
bbb651 c523c80598 Support WAYLAND_SOCKET in winit backend
I know of a single compositor that supports `WAYLAND_SOCKET` but not
`WAYLAND_DISPLAY`: https://gitlab.freedesktop.org/mstoeckl/windowtolayer

This should also make niri more robust against accidentally setting
`WAYLAND_SOCKET` when starting as a session, before programs could fail
if they preffered `WAYLAND_SOCKET` over `WAYLAND_DISPLAY`
2025-01-13 08:19:17 +03:00
mrheinen 0bd6df507b Highlight that the path in niri.service should be checked (#962)
* Highlight that the path in niri.service should be checked

Having just installed niri I ran into this issue.  When building from source on Ubuntu the install location using the instructions in this document is /usr/local//bin/niri.

However niri.service pointed to /usr/bin/niri so my session would not start at all. Hopefully this update helps

* Update wiki/Getting-Started.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-13 06:59:21 +03:00
sodiboo 6e41220dbf use standard padding syntax instead of implementing our own
the padding of the two-digit-month can be implemented much more
concisely using `std::fmt` syntax.
2025-01-12 21:38:51 +03:00
2062 changed files with 41422 additions and 6448 deletions
+15 -1
View File
@@ -9,9 +9,23 @@ 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. -->
<!--
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.
You can also check what process the window PID belongs to:
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
If this points to xwayland-satellite, then it's an X11 window.
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
-->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
* niri version:
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
+6
View File
@@ -2,3 +2,9 @@ contact_links:
- name: Feature request
url: https://github.com/YaLTeR/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
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
about: Chat about niri with other users
+57 -8
View File
@@ -9,6 +9,8 @@ on:
env:
RUN_SLOW_TESTS: 1
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
jobs:
build:
@@ -33,7 +35,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
@@ -68,6 +70,41 @@ jobs:
- name: Test
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
# Job that runs randomized tests for a longer period of time.
randomized-tests:
strategy:
fail-fast: false
name: randomized tests
runs-on: ubuntu-24.04
env:
RUST_BACKTRACE: 1
PROPTEST_CASES: 200000
PROPTEST_MAX_LOCAL_REJECTS: 200000
PROPTEST_MAX_GLOBAL_REJECTS: 200000
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests --release
- name: Test
run: cargo test --all --exclude niri-visual-tests --release
visual-tests:
strategy:
fail-fast: false
@@ -83,7 +120,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
@@ -96,7 +133,7 @@ jobs:
strategy:
fail-fast: false
name: 'msrv - 1.80.1'
name: msrv
runs-on: ubuntu-24.04
steps:
@@ -107,7 +144,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@1.80.1
@@ -130,7 +167,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -168,7 +205,7 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libdisplay-info-devel libadwaita-devel
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
@@ -191,9 +228,21 @@ jobs:
- run: nix flake check
continue-on-error: true
check-links:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
with:
args: --offline --include-fragments 'wiki/*.md'
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
needs:
- build
- check-links
permissions:
contents: write
runs-on: ubuntu-24.04
@@ -202,7 +251,7 @@ jobs:
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
rustdoc:
needs: build
Generated
+471 -446
View File
File diff suppressed because it is too large Load Diff
+27 -24
View File
@@ -6,31 +6,33 @@ members = [
]
[workspace.package]
version = "25.1.0"
version = "25.2.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"
rust-version = "1.80.1"
[workspace.dependencies]
anyhow = "1.0.95"
bitflags = "2.7.0"
clap = { version = "4.5.26", features = ["derive"] }
insta = "1.42.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
anyhow = "1.0.97"
bitflags = "2.9.0"
clap = { version = "4.5.34", features = ["derive"] }
insta = "1.42.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracy-client = { version = "0.18.0", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
@@ -54,30 +56,31 @@ async-channel = "2.3.1"
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.21.0", features = ["derive"] }
bytemuck = { version = "1.22.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
clap_complete = "4.5.47"
directories = "6.0.0"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.29.2"
glam = "0.30.1"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.169"
libc = "0.2.171"
libdisplay-info = "0.2.2"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.1.0", path = "niri-config" }
niri-ipc = { version = "25.1.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "4.6.0"
pango = { version = "0.20.7", features = ["v1_44"] }
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.2.0", path = "niri-config" }
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.0.0"
pango = { version = "0.20.9", features = ["v1_44"] }
pangocairo = "0.20.7"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.16"
portable-atomic = { version = "1.10.0", default-features = false, features = ["float"] }
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.3"
sd-notify = "0.4.5"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
@@ -85,10 +88,10 @@ tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
wayland-backend = "0.3.8"
wayland-scanner = "0.31.6"
xcursor = "0.3.8"
zbus = { version = "5.2.0", optional = true }
zbus = { version = "5.5.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -115,7 +118,7 @@ insta.workspace = true
proptest = "1.6.0"
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
rayon = "1.10.0"
wayland-client = "0.31.7"
wayland-client = "0.31.8"
xshell = "0.2.7"
[features]
@@ -149,7 +152,7 @@ insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "25.01"
version = "25.02"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+9 -1
View File
@@ -29,11 +29,12 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- Dynamic workspaces like in GNOME
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [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://github.com/YaLTeR/niri/wiki/Tabs)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#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)
@@ -91,6 +92,13 @@ Here are some other projects which implement a similar workflow:
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Media
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
Generated
+6 -6
View File
@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733064805,
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
"lastModified": 1742707865,
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1733106880,
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
"lastModified": 1742697269,
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"type": "github"
},
"original": {
+9 -2
View File
@@ -32,13 +32,14 @@
libinput,
seatd,
libxkbcommon,
mesa,
libgbm,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
installShellFiles,
withDbus ? true,
withSystemd ? true,
withScreencastSupport ? true,
@@ -79,6 +80,7 @@
nativeBuildInputs = [
rustPlatform.bindgenHook
pkg-config
installShellFiles
];
buildInputs =
@@ -90,7 +92,7 @@
libinput
seatd
libxkbcommon
mesa # libgbm
libgbm
pango
wayland
]
@@ -117,6 +119,11 @@
postInstall =
''
installShellCompletion --cmd niri \
--bash <($out/bin/niri completions bash) \
--fish <($out/bin/niri completions fish) \
--zsh <($out/bin/niri completions zsh)
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
''
+2 -3
View File
@@ -11,8 +11,8 @@ repository.workspace = true
bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "25.1.0", path = "../niri-ipc" }
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
@@ -20,5 +20,4 @@ tracy-client.workspace = true
[dev-dependencies]
insta.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
+5 -1
View File
@@ -1,4 +1,4 @@
use crate::{BlockOutFrom, RegexEq};
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
@@ -11,6 +11,10 @@ pub struct LayerRule {
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+1669 -328
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "0.8.21", optional = true }
schemars = { version = "0.8.22", optional = true }
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.1.0"
niri-ipc = "=25.2.0"
```
+232 -6
View File
@@ -24,7 +24,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.1.0"
//! niri-ipc = "=25.2.0"
//! ```
//!
//! ## Features
@@ -63,6 +63,10 @@ pub enum Request {
FocusedOutput,
/// Request information about the focused window.
FocusedWindow,
/// Request picking a window and get its information.
PickWindow,
/// Request picking a color from the screen.
PickColor,
/// Perform an action.
Action(Action),
/// Change output configuration temporarily.
@@ -129,10 +133,22 @@ pub enum Response {
FocusedOutput(Option<Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
/// Information about the picked window.
PickedWindow(Option<Window>),
/// Information about the picked color.
PickedColor(Option<PickedColor>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
}
/// Color picked from the screen.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PickedColor {
/// Color values as red, green, blue, each ranging from 0.0 to 1.0.
pub rgb: [f64; 3],
}
/// Actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
// variants from niri-config should be present here.
@@ -165,9 +181,23 @@ pub enum Action {
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot {},
Screenshot {
/// Whether to show the mouse pointer by default in the screenshot UI.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
/// Screenshot the focused screen.
ScreenshotScreen {},
ScreenshotScreen {
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[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.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
ScreenshotWindow {
@@ -176,7 +206,14 @@ pub enum Action {
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
},
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
ToggleKeyboardShortcutsInhibit {},
/// Close a window.
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
CloseWindow {
@@ -198,12 +235,32 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle windowed (fake) fullscreen on a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
)]
ToggleWindowedFullscreen {
/// Id of the window to toggle windowed fullscreen of.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus a window by id.
FocusWindow {
/// Id of the window to focus.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus a window in the focused column by index.
FocusWindowInColumn {
/// Index of the window in the column.
///
/// The index starts from 1 for the topmost window.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Focus the previously focused window.
FocusWindowPrevious {},
/// Focus the column to the left.
@@ -218,6 +275,14 @@ pub enum Action {
FocusColumnRightOrFirst {},
/// Focus the next column to the left, looping if at start.
FocusColumnLeftOrLast {},
/// Focus a column by index.
FocusColumn {
/// Index of the column to focus.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Focus the window or the monitor above.
FocusWindowOrMonitorUp {},
/// Focus the window or the monitor below.
@@ -242,6 +307,14 @@ pub enum Action {
FocusWindowOrWorkspaceDown {},
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp {},
/// Focus the topmost window.
FocusWindowTop {},
/// Focus the bottommost window.
FocusWindowBottom {},
/// Focus the window below or the topmost window.
FocusWindowDownOrTop {},
/// Focus the window above or the bottommost window.
FocusWindowUpOrBottom {},
/// Move the focused column to the left.
MoveColumnLeft {},
/// Move the focused column to the right.
@@ -254,6 +327,14 @@ pub enum Action {
MoveColumnLeftOrToMonitorLeft {},
/// Move the focused column to the right or to the monitor to the right.
MoveColumnRightOrToMonitorRight {},
/// Move the focused column to a specific index on its workspace.
MoveColumnToIndex {
/// New index for the column.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Move the focused window down in a column.
MoveWindowDown {},
/// Move the focused window up in a column.
@@ -290,10 +371,18 @@ pub enum Action {
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right
/// Swap focused window with one to the right.
SwapWindowRight {},
/// Swap focused window with one to the left
/// Swap focused window with one to the left.
SwapWindowLeft {},
/// Toggle the focused column between normal and tabbed display.
ToggleColumnTabbedDisplay {},
/// Set the display mode of the focused column.
SetColumnDisplay {
/// Display mode to set.
#[cfg_attr(feature = "clap", arg())]
display: ColumnDisplay,
},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
@@ -339,6 +428,14 @@ pub enum Action {
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the moved window.
///
/// If `true` (the default) and the window to move is focused, the focus will follow the
/// window to the new workspace. If `false`, the focus will remain on the original
/// workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown {},
@@ -354,6 +451,22 @@ pub enum Action {
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Move a workspace to a specific index on its monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific index on its monitor")
)]
MoveWorkspaceToIndex {
/// New index for the workspace.
#[cfg_attr(feature = "clap", arg())]
index: usize,
/// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
@@ -394,6 +507,12 @@ pub enum Action {
FocusMonitorPrevious {},
/// Focus the next monitor.
FocusMonitorNext {},
/// Focus a monitor by name.
FocusMonitor {
/// Name of the output to focus.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
@@ -406,6 +525,22 @@ pub enum Action {
MoveWindowToMonitorPrevious {},
/// Move the focused window to the next monitor.
MoveWindowToMonitorNext {},
/// Move a window to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused window to a specific monitor")
)]
MoveWindowToMonitor {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
@@ -418,6 +553,12 @@ pub enum Action {
MoveColumnToMonitorPrevious {},
/// Move the focused column to the next monitor.
MoveColumnToMonitorNext {},
/// Move the focused column to a specific monitor.
MoveColumnToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Change the width of a window.
#[cfg_attr(
feature = "clap",
@@ -488,6 +629,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Expand the focused column to space not taken up by other fully visible columns.
ExpandColumnToAvailableWidth {},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
@@ -508,6 +651,22 @@ pub enum Action {
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
/// Move a workspace to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific monitor")
)]
MoveWorkspaceToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
@@ -567,6 +726,46 @@ pub enum Action {
)]
y: PositionChange,
},
/// Toggle the opacity of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle the opacity of the focused window")
)]
ToggleWindowRuleOpacity {
/// Id of the window.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a window.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused window")
)]
SetDynamicCastWindow {
/// Id of the window to target.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused monitor")
)]
SetDynamicCastMonitor {
/// Name of the output to target.
///
/// If `None`, uses the focused output.
#[cfg_attr(feature = "clap", arg())]
output: Option<String>,
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle the Overview.
ToggleOverview {},
}
/// Change in window or column size.
@@ -613,6 +812,18 @@ pub enum LayoutSwitchTarget {
Next,
/// The previous configured layout.
Prev,
/// The specific layout by index.
Index(u8),
}
/// How windows display in a column.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ColumnDisplay {
/// Windows are tiled vertically across the working area height.
Normal,
/// Windows are in tabs.
Tabbed,
}
/// Output actions that niri can perform.
@@ -1119,7 +1330,22 @@ impl FromStr for LayoutSwitchTarget {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
other => match other.parse() {
Ok(layout) => Ok(Self::Index(layout)),
_ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
},
}
}
}
impl FromStr for ColumnDisplay {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"tabbed" => Ok(Self::Tabbed),
_ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
}
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.5", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.1.0", path = ".." }
niri-config = { version = "25.1.0", path = "../niri-config" }
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.2.0", path = ".." }
niri-config = { version = "25.2.0", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
@@ -63,6 +63,7 @@ impl TestCase for GradientAngle {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -84,6 +84,7 @@ impl TestCase for GradientArea {
Rectangle::default(),
CornerRadius::default(),
1.,
1.,
);
rv.extend(
self.border
@@ -103,6 +104,7 @@ impl TestCase for GradientArea {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklab {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -42,6 +42,7 @@ impl TestCase for GradientOklabAlpha {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchAlpha {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchDecreasing {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchIncreasing {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchLonger {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchShorter {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientSrgb {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -42,6 +42,7 @@ impl TestCase for GradientSrgbAlpha {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -44,6 +44,7 @@ impl TestCase for GradientSrgbLinear {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -42,6 +42,7 @@ impl TestCase for GradientSrgbLinearAlpha {
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+17 -15
View File
@@ -2,10 +2,9 @@ use std::collections::HashMap;
use std::time::Duration;
use niri::animation::Clock;
use niri::layout::scrolling::ColumnWidth;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt, OutputName};
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
@@ -84,13 +83,13 @@ impl Layout {
pub fn open_in_between(args: Args) -> Self {
let mut rv = Self::new(args);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
rv.layout.activate_window(&0);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
@@ -103,7 +102,7 @@ impl Layout {
for delay in [100, 200, 300] {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -117,7 +116,7 @@ impl Layout {
for delay in [100, 200, 300] {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -128,13 +127,13 @@ impl Layout {
pub fn open_to_the_left(args: Args) -> Self {
let mut rv = Self::new(args);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
@@ -144,26 +143,27 @@ impl Layout {
pub fn open_to_the_left_big(args: Args) -> Self {
let mut rv = Self::new(args);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
rv
}
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.communicate();
@@ -184,7 +184,7 @@ impl Layout {
&mut self,
right_of: &TestWindow,
mut window: TestWindow,
width: Option<ColumnWidth>,
width: Option<PresetSize>,
) {
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
@@ -192,6 +192,7 @@ impl Layout {
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.communicate();
@@ -265,6 +266,7 @@ impl TestCase for Layout {
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _)
.collect()
}
+4 -9
View File
@@ -6,7 +6,7 @@ use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Scale, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
use crate::test_window::TestWindow;
@@ -112,18 +112,13 @@ impl TestCase for Tile {
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update(
self.tile.update_render_elements(
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
1.,
);
self.tile
.render(
renderer,
location,
Scale::from(1.),
true,
RenderTarget::Output,
)
.render(renderer, location, true, RenderTarget::Output)
.map(|elem| Box::new(elem) as _)
.collect()
}
+4 -4
View File
@@ -14,14 +14,14 @@ pub struct Window {
impl Window {
pub fn freeform(args: Args) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(args.size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(args.size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
@@ -29,7 +29,7 @@ impl Window {
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(args.size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
@@ -38,7 +38,7 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window
.request_size(Size::from((width, height)), false, None);
.request_size(Size::from((width, height)), false, false, None);
self.window.communicate();
}
+56 -40
View File
@@ -17,19 +17,25 @@ mod imp {
use niri::render_helpers::{resources, shaders};
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
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::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
struct RendererData {
renderer: GlesRenderer,
dummy_texture: GlesTexture,
}
#[derive(Default)]
pub struct SmithayView {
gl_area: gtk::GLArea,
size: Cell<(i32, i32)>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
renderer: RefCell<Option<Result<RendererData, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
pub clock: RefCell<Clock>,
@@ -125,6 +131,10 @@ mod imp {
let Ok(renderer) = renderer else {
return Ok(());
};
let RendererData {
renderer,
dummy_texture,
} = renderer;
let size = self.size.get();
@@ -147,16 +157,45 @@ mod imp {
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
case.render(renderer, Size::from(size))
// Fetch GtkGLArea's framebuffer binding.
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
}?;
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
// This call will already change the framebuffer binding (offscreen elements will bind
// intermediate textures during rendering).
let elements = case.render(renderer, Size::from(size));
// HACK: there's currently no way to "just" render into an externally bound framebuffer
// (like we have in this case). The render() call requires a valid target. So what
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
// before rendering.
let mut dummy_target = renderer
.bind(dummy_texture)
.context("error binding dummy texture")?;
let mut frame = renderer
.render(rect.size, Transform::Normal)
.render(&mut dummy_target, rect.size, Transform::Normal)
.context("error creating frame")?;
// Now that render() bound the dummy texture, change the binding underneath it back to
// GtkGLArea's framebuffer, to render there instead.
frame
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
frame
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
.context("error clearing")?;
@@ -177,7 +216,7 @@ mod imp {
}
}
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
@@ -200,40 +239,17 @@ mod imp {
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
let dummy_texture = renderer
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
.context("error creating dummy texture")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
Ok(renderer)
}
unsafe fn with_framebuffer_save_restore<T>(
renderer: &mut GlesRenderer,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> anyhow::Result<T> {
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
let rv = f(renderer);
renderer.unbind().context("error unbinding")?;
renderer
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
Ok(rv)
Ok(RendererData {
renderer,
dummy_texture,
})
}
}
+9 -7
View File
@@ -6,12 +6,13 @@ use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
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::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::backend::renderer::element::Kind;
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
@@ -181,15 +182,12 @@ impl LayoutElement for TestWindow {
fn request_size(
&mut self,
size: Size<i32, Logical>,
is_fullscreen: bool,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
}
fn min_size(&self) -> Size<i32, Logical> {
@@ -214,7 +212,7 @@ impl LayoutElement for TestWindow {
fn output_leave(&self, _output: &Output) {}
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
fn set_activated(&mut self, _active: bool) {}
@@ -224,6 +222,10 @@ impl LayoutElement for TestWindow {
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn is_ignoring_opacity_window_rule(&self) -> bool {
false
}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
+2 -2
View File
@@ -54,7 +54,7 @@ SourceLicense: GPL-3.0-or-later
# Unicode-3.0
# Unlicense OR MIT
# Zlib OR Apache-2.0 OR MIT
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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 AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) 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 OR Apache-2.0 OR MIT) License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) 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 OR Apache-2.0 OR MIT)
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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 AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) 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 OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
@@ -89,6 +89,7 @@ Recommends: gnome-keyring
Recommends: alacritty
Recommends: fuzzel
Recommends: swaylock
Recommends: waybar
# Suggested utilities
Recommends: swaybg
Recommends: mako
@@ -128,7 +129,6 @@ install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
%if %{with check}
%check
export XDG_RUNTIME_DIR="$(mktemp -d)"
%cargo_test -- --workspace --exclude niri-visual-tests
%endif
+65 -4
View File
@@ -25,6 +25,8 @@ input {
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
@@ -191,6 +193,43 @@ layout {
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can enable drop shadows for windows.
shadow {
// Uncomment the next line to enable shadows.
// on
// By default, the shadow draws only around its window, and not behind it.
// Uncomment this setting to make the shadow draw behind its window.
//
// Note that niri has no way of knowing about the CSD window corner
// radius. It has to assume that windows have square corners, leading to
// shadow artifacts inside the CSD rounded corners. This setting fixes
// those artifacts.
//
// However, instead you may want to set prefer-no-csd and/or
// geometry-corner-radius. Then, niri will know the corner radius and
// draw the shadow correctly, without having to draw it behind the
// window. These will also remove client-side shadows if the window
// draws any.
//
// draw-behind-window true
// You can change how shadows look. The values below are in logical
// pixels and match the CSS box-shadow properties.
// Softness controls the shadow blur radius.
softness 30
// Spread expands the shadow.
spread 5
// Offset moves the shadow relative to the window.
offset x=0 y=5
// You can also change the shadow color and opacity.
color "#0007"
}
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
@@ -208,7 +247,9 @@ layout {
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// spawn-at-startup "alacritty" "-e" "fish"
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "waybar"
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
@@ -294,9 +335,9 @@ binds {
Mod+Shift+Slash { show-hotkey-overlay; }
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Super+Alt+L { spawn "swaylock"; }
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
@@ -466,6 +507,11 @@ binds {
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// 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; }
Mod+C { center-column; }
// Finer width adjustments.
@@ -487,6 +533,11 @@ binds {
Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
// Toggle tabbed column display mode.
// Windows in this column will appear as vertical tabs,
// rather than stacked on top of each other.
Mod+W { toggle-column-tabbed-display; }
// Actions to switch layouts.
// Note: if you uncomment these, make sure you do NOT have
// a matching layout switch hotkey configured in xkb options above.
@@ -499,6 +550,16 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// Applications such as remote-desktop clients and software KVM switches may
// request that niri stops processing the keyboard shortcuts defined here
// so they may, for example, forward the key presses as-is to a remote machine.
// It's a good idea to bind an escape hatch to toggle the inhibitor,
// so a buggy application can't hold your session hostage.
//
// The allow-inhibiting=false property can be applied to other binds as well,
// which ensures niri always processes them, even when an inhibitor is active.
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
+4 -4
View File
@@ -12,7 +12,7 @@ if [ -n "$SHELL" ] &&
fi
# Try to detect the service manager that is being used
if hash systemctl &> /dev/null; then
if hash systemctl >/dev/null 2>&1; then
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
@@ -41,15 +41,15 @@ if hash systemctl &> /dev/null; then
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
elif hash dinitctl &> /dev/null; then
elif hash dinitctl >/dev/null 2>&1; then
# Check that the user dinit daemon is running
if ! pgrep -u $(id -u) dinit &> /dev/null; then
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
echo "dinit user daemon is not running."
exit 1
fi
# Make sure there's no already running session.
if dinitctl --user is-started niri &> /dev/null; then
if dinitctl --user is-started niri >/dev/null 2>&1; then
echo 'A niri session is already running.'
exit 1
fi
+30 -6
View File
@@ -123,8 +123,8 @@ impl Animation {
),
Kind::Spring(spring) => {
let spring = Spring {
from: self.from,
to: self.to,
from,
to,
initial_velocity: self.initial_velocity,
params: spring.params,
};
@@ -242,12 +242,21 @@ impl Animation {
self.clock.now() >= self.start_time + self.clamped_duration
}
pub fn value(&self) -> f64 {
if self.is_done() {
pub fn value_at(&self, at: Duration) -> f64 {
if at <= self.start_time {
// Return from when at == start_time so that when the animations are off, the behavior
// within a single event loop cycle (i.e. no time had passed since the start of an
// animation) matches the behavior when the animations are on.
return self.from;
} else if self.start_time + self.duration <= at {
return self.to;
}
let passed = self.clock.now().saturating_sub(self.start_time);
if self.clock.should_complete_instantly() {
return self.to;
}
let passed = at.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
@@ -280,6 +289,10 @@ impl Animation {
}
}
pub fn value(&self) -> f64 {
self.value_at(self.clock.now())
}
/// Returns a value that stops at the target value after first reaching it.
///
/// Best effort; not always exactly precise.
@@ -295,11 +308,22 @@ impl Animation {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
pub fn start_time(&self) -> Duration {
self.start_time
}
pub fn end_time(&self) -> Duration {
self.start_time + self.duration
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn offset(&mut self, offset: f64) {
self.from += offset;
self.to += offset;
+22
View File
@@ -54,6 +54,10 @@ impl Spring {
return Duration::MAX;
}
if (self.to - self.from).abs() <= f64::EPSILON {
return Duration::ZERO;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
@@ -166,3 +170,21 @@ impl Spring {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overdamped_spring_equal_from_to_nan() {
let spring = Spring {
from: 0.,
to: 0.,
initial_velocity: 0.,
params: SpringParams::new(1.15, 850., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
}
+10 -5
View File
@@ -2,12 +2,12 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, ModKey};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
@@ -94,11 +94,16 @@ impl Backend {
}
}
pub fn mod_key(&self) -> CompositorMod {
pub fn mod_key(&self, config: &Config) -> ModKey {
match self {
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
Backend::Headless(_) => CompositorMod::Super,
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
if let Some(ModKey::Alt) = config.input.mod_key {
ModKey::Super
} else {
ModKey::Alt
}
}),
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
}
}
+6 -5
View File
@@ -28,7 +28,7 @@ use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
@@ -101,15 +101,16 @@ pub type TtyRenderer<'render> = MultiRenderer<
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyFrame<'render, 'frame> = MultiFrame<
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
'render,
'render,
'frame,
'buffer,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
type GbmDrmCompositor = DrmCompositor<
GbmAllocator<DrmDeviceFd>,
@@ -478,7 +479,7 @@ impl Tty {
self.refresh_ipc_outputs(niri);
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.notify_activity();
niri.monitors_active = true;
self.set_monitors_active(true);
niri.queue_redraw_all();
@@ -547,7 +548,7 @@ impl Tty {
}
drop(config);
niri.layout.update_shaders();
niri.update_shaders();
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats();
+11 -7
View File
@@ -156,7 +156,7 @@ impl Winit {
}
drop(config);
niri.layout.update_shaders();
niri.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
@@ -190,12 +190,16 @@ impl Winit {
}
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
let res = self
.damage_tracker
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
.unwrap();
let res = {
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
// FIXME: currently impossible to call due to a mutable borrow.
//
// let age = self.backend.buffer_age().unwrap();
let age = 0;
self.damage_tracker
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
.unwrap()
};
niri.update_primary_scanout_output(output, &res.states);
+7
View File
@@ -2,6 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -54,6 +55,8 @@ pub enum Sub {
},
/// Cause a panic to check if the backtraces are good.
Panic,
/// Generate shell completions.
Completions { shell: Shell },
}
#[derive(Subcommand)]
@@ -72,6 +75,10 @@ pub enum Msg {
FocusedOutput,
/// Print information about the focused window.
FocusedWindow,
/// Pick a window with the mouse and print information about it.
PickWindow,
/// Pick a color from the screen with the mouse.
PickColor,
/// Perform an action.
Action {
#[command(subcommand)]
+17 -15
View File
@@ -4,12 +4,11 @@ use std::env;
use std::fs::File;
use std::io::Read;
use std::rc::Rc;
use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
use smithay::wayland::compositor::with_states;
@@ -67,7 +66,7 @@ impl CursorManager {
let hotspot = with_states(&surface, |states| {
states
.data_map
.get::<Mutex<CursorImageAttributes>>()
.get::<CursorImageSurfaceData>()
.unwrap()
.lock()
.unwrap()
@@ -76,21 +75,24 @@ impl CursorManager {
RenderCursor::Surface { hotspot, surface }
}
CursorImageStatus::Named(icon) => self
.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
}),
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
}
}
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
self.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
})
}
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
match &self.current_cursor {
CursorImageStatus::Hidden => false,
+33 -1
View File
@@ -1,7 +1,10 @@
use std::collections::HashMap;
use std::path::PathBuf;
use niri_ipc::PickedColor;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::zvariant::OwnedValue;
use zbus::{interface, zvariant};
use super::Start;
@@ -12,6 +15,7 @@ pub struct Screenshot {
pub enum ScreenshotToNiri {
TakeScreenshot { include_cursor: bool },
PickColor(async_channel::Sender<Option<PickedColor>>),
}
pub enum NiriToScreenshot {
@@ -47,6 +51,34 @@ impl Screenshot {
Ok((true, filename))
}
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
let (tx, rx) = async_channel::bounded(1);
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
warn!("error sending pick color message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
let color = match rx.recv().await {
Ok(Some(color)) => color,
Ok(None) => {
return Err(fdo::Error::Failed("no color picked".to_owned()));
}
Err(err) => {
warn!("error receiving message from niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
};
let mut result = HashMap::new();
let [r, g, b] = color.rgb;
result.insert(
"color".to_string(),
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
);
Ok(result)
}
}
impl Screenshot {
+29 -2
View File
@@ -45,12 +45,39 @@ impl DBusServers {
let mut dbus = Self::default();
if is_session_instance {
let service_channel = ServiceChannel::new(niri.display_handle.clone());
let (to_niri, from_service_channel) = calloop::channel::channel();
let service_channel = ServiceChannel::new(to_niri);
niri.event_loop
.insert_source(from_service_channel, move |event, _, state| match event {
calloop::channel::Event::Msg(new_client) => {
state.niri.insert_client(new_client);
}
calloop::channel::Event::Closed => (),
})
.unwrap();
dbus.conn_service_channel = try_start(service_channel);
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.ipc_outputs());
let (to_niri, from_display_config) = calloop::channel::channel();
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
niri.event_loop
.insert_source(from_display_config, move |event, _, state| match event {
calloop::channel::Event::Msg(new_conf) => {
for (name, conf) in new_conf {
state.modify_output_config(&name, move |output| {
if let Some(new_output) = conf {
*output = new_output;
} else {
output.off = true;
}
});
}
state.reload_output_config();
}
calloop::channel::Event::Closed => (),
})
.unwrap();
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
+197 -71
View File
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use smithay::utils::Size;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{self, OwnedValue, Type};
@@ -10,8 +12,10 @@ use zbus::{fdo, interface};
use super::Start;
use crate::backend::IpcOutputMap;
use crate::utils::is_laptop_panel;
use crate::utils::scale::supported_scales;
pub struct DisplayConfig {
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -44,6 +48,17 @@ pub struct LogicalMonitor {
properties: HashMap<String, OwnedValue>,
}
// ApplyMonitorsConfig
#[derive(Deserialize, Type)]
pub struct LogicalMonitorConfiguration {
x: i32,
y: i32,
scale: f64,
transform: u32,
_is_primary: bool,
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
}
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
@@ -55,75 +70,70 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.values()
// Take only enabled outputs.
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
.map(|output| {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut monitors = Vec::new();
let mut logical_monitors = Vec::new();
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
for output in self.ipc_outputs.lock().unwrap().values() {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let refresh = refresh_rate as f64 / 1000.;
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
Mode {
id: format!("{width}x{height}@{refresh:.3}"),
width: i32::from(width),
height: i32::from(height),
refresh_rate: refresh,
preferred_scale: 1.,
supported_scales: vec![1., 2., 3.],
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
modes[output.current_mode.unwrap()]
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let width = i32::from(width);
let height = i32::from(height);
let refresh_rate = refresh_rate as f64 / 1000.;
Mode {
id: format!("{width}x{height}@{refresh_rate:.3}"),
width,
height,
refresh_rate,
preferred_scale: 1.,
supported_scales: supported_scales(Size::from((width, height))).collect(),
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
if let Some(mode) = output.current_mode {
modes[mode]
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
}
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let monitor = Monitor {
names: (connector, make, model, serial),
modes,
properties,
};
let logical = output.logical.as_ref().unwrap();
let names = (connector, make, model, serial);
if let Some(logical) = output.logical.as_ref() {
let transform = match logical.transform {
niri_ipc::Transform::Normal => 0,
niri_ipc::Transform::_90 => 1,
@@ -135,35 +145,151 @@ impl DisplayConfig {
niri_ipc::Transform::Flipped270 => 7,
};
let logical_monitor = LogicalMonitor {
logical_monitors.push(LogicalMonitor {
x: logical.x,
y: logical.y,
scale: logical.scale,
transform,
is_primary: false,
monitors: vec![monitor.names.clone()],
monitors: vec![names.clone()],
properties: HashMap::new(),
};
});
}
(monitor, logical_monitor)
})
.collect();
monitors.push(Monitor {
names,
modes,
properties,
});
}
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
async fn apply_monitors_config(
&self,
_serial: u32,
method: u32,
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
_properties: HashMap<String, OwnedValue>,
) -> fdo::Result<()> {
let current_conf = self.ipc_outputs.lock().unwrap();
let mut new_conf = HashMap::new();
for requested_config in logical_monitor_configs {
if requested_config.monitors.len() > 1 {
return Err(zbus::fdo::Error::Failed(
"Mirroring is not yet supported".to_owned(),
));
}
for (connector, mode, _props) in requested_config.monitors {
if !current_conf.values().any(|o| o.name == connector) {
return Err(zbus::fdo::Error::Failed(format!(
"Connector '{}' not found",
connector
)));
}
new_conf.insert(
connector.clone(),
Some(niri_config::Output {
off: false,
name: connector,
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
transform: match requested_config.transform {
0 => niri_ipc::Transform::Normal,
1 => niri_ipc::Transform::_90,
2 => niri_ipc::Transform::_180,
3 => niri_ipc::Transform::_270,
4 => niri_ipc::Transform::Flipped,
5 => niri_ipc::Transform::Flipped90,
6 => niri_ipc::Transform::Flipped180,
7 => niri_ipc::Transform::Flipped270,
x => {
return Err(zbus::fdo::Error::Failed(format!(
"Unknown transform {}",
x
)))
}
},
position: Some(niri_config::Position {
x: requested_config.x,
y: requested_config.y,
}),
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
zbus::fdo::Error::Failed(format!(
"Could not parse mode '{}': {}",
mode, e
))
})?),
// FIXME: VRR
..Default::default()
}),
);
}
}
if new_conf.is_empty() {
return Err(zbus::fdo::Error::Failed(
"At least one output must be enabled".to_owned(),
));
}
for output in current_conf.values() {
if !new_conf.contains_key(&output.name) {
new_conf.insert(output.name.clone(), None);
}
}
if method == 0 {
// 0 means "verify", so don't actually apply here
return Ok(());
}
if let Err(err) = self.to_niri.send(new_conf) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(())
}
#[zbus(signal)]
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[zbus(property)]
fn power_save_mode(&self) -> i32 {
-1
}
#[zbus(property)]
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
Err(zbus::Error::Unsupported)
}
#[zbus(property)]
fn panel_orientation_managed(&self) -> bool {
false
}
#[zbus(property)]
fn apply_monitors_config_allowed(&self) -> bool {
true
}
#[zbus(property)]
fn night_light_supported(&self) -> bool {
false
}
}
impl DisplayConfig {
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
pub fn new(
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
) -> Self {
Self {
to_niri,
ipc_outputs,
}
}
}
+29 -13
View File
@@ -62,6 +62,8 @@ static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct Stream {
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
@@ -93,6 +95,7 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
stream_id: usize,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalEmitter<'static>,
@@ -149,7 +152,7 @@ impl Session {
debug!("start");
for (stream, iface) in &*self.streams.lock().unwrap() {
stream.start(self.id, iface.signal_emitter().clone());
stream.start(iface.signal_emitter().clone());
}
}
@@ -204,16 +207,20 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let target = StreamTarget::Output(output);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -237,10 +244,8 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(?properties, "record_window");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
@@ -248,7 +253,13 @@ impl Session {
let target = StreamTarget::Window {
id: properties.window_id,
};
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -350,11 +361,15 @@ impl Drop for Session {
impl Stream {
fn new(
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
session_id,
target,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
@@ -362,13 +377,14 @@ impl Stream {
}
}
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
fn start(&self, ctxt: SignalEmitter<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
let msg = ScreenCastToNiri::StartCast {
session_id,
session_id: self.session_id,
stream_id: self.id,
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
+12 -12
View File
@@ -1,14 +1,12 @@
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use smithay::reexports::wayland_server::DisplayHandle;
use zbus::{fdo, interface, zvariant};
use super::Start;
use crate::niri::ClientState;
use crate::niri::NewClient;
pub struct ServiceChannel {
display: DisplayHandle,
to_niri: calloop::channel::Sender<NewClient>,
}
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
@@ -24,22 +22,24 @@ impl ServiceChannel {
}
let (sock1, sock2) = UnixStream::pair().unwrap();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
let client = NewClient {
client: sock2,
restricted: false,
// FIXME: maybe you can get the PID from D-Bus somehow?
credentials_unknown: true,
});
self.display.insert_client(sock2, data).unwrap();
};
if let Err(err) = self.to_niri.send(client) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
}
}
impl ServiceChannel {
pub fn new(display: DisplayHandle) -> Self {
Self { display }
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
Self { to_niri }
}
}
+32 -33
View File
@@ -1,7 +1,7 @@
use std::collections::hash_map::Entry;
use niri_ipc::PositionChange;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
@@ -21,9 +21,9 @@ use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
use crate::layout::{ActivateWindow, AddWindowTarget};
use crate::niri::{ClientState, State};
use crate::utils::send_scale_transform;
use crate::niri::{CastTarget, ClientState, LockState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{is_mapped, send_scale_transform};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
@@ -78,14 +78,7 @@ impl CompositorHandler for State {
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()) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
if is_mapped(surface) {
// The toplevel got mapped.
let Unmapped {
window,
@@ -166,7 +159,9 @@ impl CompositorHandler for State {
// None. If the configured output is set, that means it was set explicitly
// by a window rule or a fullscreen request.
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
parent_output.is_none()
|| output.is_none()
|| output.as_ref() == *parent_output
})
.map(|(mapped, _)| mapped.window.clone());
@@ -223,18 +218,12 @@ impl CompositorHandler for State {
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
let is_mapped = is_mapped(surface);
// Must start the close animation before window.on_commit().
let transaction = Transaction::new();
@@ -256,11 +245,8 @@ impl CompositorHandler for State {
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: id.get(),
});
.stop_casts_for_target(CastTarget::Window { id: id.get() });
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface);
@@ -280,7 +266,9 @@ impl CompositorHandler for State {
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
@@ -323,7 +311,9 @@ impl CompositorHandler for State {
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window);
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
@@ -334,10 +324,12 @@ impl CompositorHandler for State {
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
window.on_commit();
self.niri.layout.update_window(&window, None);
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
@@ -412,16 +404,23 @@ impl CompositorHandler for State {
}
// This might be a lock surface.
if self.niri.is_locked() {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
self.niri.maybe_continue_to_locking();
} else {
self.niri.queue_redraw(&output.clone());
return;
}
return;
}
}
}
// This message can trigger for lock surfaces that had a commit right after we unlocked
// the session, but that's ok, we don't need to handle them.
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
}
fn destroyed(&mut self, surface: &WlSurface) {
+5 -12
View File
@@ -1,4 +1,3 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
@@ -13,7 +12,7 @@ use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;
use crate::utils::{is_mapped, send_scale_transform};
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -120,22 +119,16 @@ impl State {
.unwrap();
if initial_configure_sent {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
if is_mapped(surface) {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let rules = &self.niri.config.borrow().layer_rules;
let config = self.niri.config.borrow();
let rules = &config.layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let mapped = MappedLayer::new(layer.clone(), rules);
let mapped = MappedLayer::new(layer.clone(), rules, &config);
let prev = self
.niri
.mapped_layer_surfaces
+130 -40
View File
@@ -11,7 +11,7 @@ use std::time::Duration;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::backend::input::TabletToolDescriptor;
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
@@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::keyboard_shortcuts_inhibit::{
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
@@ -44,10 +47,15 @@ use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
};
use smithay::wayland::selection::ext_data_control::{
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
};
use smithay::wayland::selection::primary_selection::{
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
};
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
use smithay::wayland::selection::wlr_data_control::{
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
};
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
@@ -58,8 +66,9 @@ use smithay::wayland::xdg_activation::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
@@ -67,7 +76,8 @@ use smithay::{
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, DndIcon, State};
use crate::layout::ActivateWindow;
use crate::niri::{DndIcon, NewClient, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
@@ -75,10 +85,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::protocols::virtual_pointer::{
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
VirtualPointerMotionEvent,
};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
@@ -243,7 +258,28 @@ impl InputMethodHandler for State {
}
}
impl KeyboardShortcutsInhibitHandler for State {
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
&mut self.niri.keyboard_shortcuts_inhibit_state
}
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
inhibitor.activate();
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.insert(inhibitor.wl_surface().clone(), inhibitor);
}
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.remove(&inhibitor.wl_surface().clone());
}
}
delegate_input_method_manager!(State);
delegate_keyboard_shortcuts_inhibit!(State);
delegate_virtual_keyboard_manager!(State);
impl SelectionHandler for State {
@@ -309,17 +345,19 @@ impl ClientDndGrabHandler for State {
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
trace!("client dropped, target: {target:?}, validated: {validated}");
// End DnD before activating a specific window below so that it takes precedence.
self.niri.layout.dnd_end();
// 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() {
if let Some(root) = self.niri.root_surface.get(&target) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
activate_output = false;
}
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);
self.niri.layer_shell_on_demand_focus = None;
activate_output = false;
}
}
@@ -335,7 +373,7 @@ impl ClientDndGrabHandler for State {
// 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.activate_output(output);
self.niri.layout.focus_output(output);
}
}
}
@@ -357,14 +395,22 @@ impl PrimarySelectionHandler for State {
}
delegate_primary_selection!(State);
impl DataControlHandler for State {
fn data_control_state(&self) -> &DataControlState {
&self.niri.data_control_state
impl WlrDataControlHandler for State {
fn data_control_state(&self) -> &WlrDataControlState {
&self.niri.wlr_data_control_state
}
}
delegate_data_control!(State);
impl ExtDataControlHandler for State {
fn data_control_state(&self) -> &ExtDataControlState {
&self.niri.ext_data_control_state
}
}
delegate_ext_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
@@ -406,9 +452,7 @@ impl SessionLockHandler for State {
fn unlock(&mut self) {
self.niri.unlock();
self.niri.activate_monitors(&mut self.backend);
self.niri
.idle_notifier_state
.notify_activity(&self.niri.seat);
self.niri.notify_activity();
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
@@ -442,19 +486,12 @@ impl SecurityContextHandler for State {
self.niri
.event_loop
.insert_source(source, move |client, _, state| {
let config = state.niri.config.borrow();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
trace!("inserting a new restricted client, context={context:?}");
state.niri.insert_client(NewClient {
client,
restricted: true,
credentials_unknown: false,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
})
.unwrap();
}
@@ -514,10 +551,13 @@ impl ForeignToplevelHandler for State {
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_to_output(Some(&window), &requested_output, None);
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
}
}
@@ -562,6 +602,31 @@ impl ScreencopyHandler for State {
}
delegate_screencopy!(State);
impl VirtualPointerHandler for State {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
&mut self.niri.virtual_pointer_state
}
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
}
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
self.process_input_event(
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
);
}
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
}
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
}
}
delegate_virtual_pointer!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
self.backend
@@ -648,7 +713,12 @@ impl XdgActivationHandler for State {
}
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
// Only tokens that were created while the application has keyboard focus are valid.
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
// common client behavior.
//
// We don't have urgency yet, so just ignore such tokens.
//
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
let Some((serial, seat)) = data.serial else {
return false;
};
@@ -656,11 +726,31 @@ impl XdgActivationHandler for State {
return false;
};
let keyboard = seat.get_keyboard().unwrap();
keyboard
.last_enter()
.map(|last_enter| serial.is_no_older_than(&last_enter))
.unwrap_or(false)
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
// upon clicking on their tray icon or on their notification. This debug flag makes that
// work.
//
// Clicking on a notification sends clients a perfectly valid activation token from the
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
// and we can remove this debug flag.
let config = self.niri.config.borrow();
if config.debug.honor_xdg_activation_with_invalid_serial {
return true;
}
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
// with no keyboard interactivity won't have any keyboard focus.
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
false
}
fn request_activation(
+73 -46
View File
@@ -1,6 +1,7 @@
use std::cell::Cell;
use calloop::Interest;
use niri_config::PresetSize;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -42,10 +43,12 @@ use crate::input::resize_grab::ResizeGrab;
use crate::input::touch_move_grab::TouchMoveGrab;
use crate::input::touch_resize_grab::TouchResizeGrab;
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
use crate::layout::scrolling::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::layout::ActivateWindow;
use crate::niri::{CastTarget, PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::utils::{
get_monotonic_time, output_matches_name, send_scale_transform, update_tiled_state, ResizeEdge,
};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -123,6 +126,10 @@ impl XdgShellHandler for State {
return;
};
let Some(output) = output else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
@@ -146,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -309,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
@@ -429,23 +439,26 @@ impl XdgShellHandler for State {
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
.find_window_and_output_mut(toplevel.wl_surface())
{
// A configure is required in response to this event regardless if there are pending
// changes.
mapped.set_needs_configure();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_to_output(Some(&window), &requested_output, None);
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -467,7 +480,7 @@ impl XdgShellHandler for State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -509,17 +522,14 @@ impl XdgShellHandler for State {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
.find_window_and_output_mut(toplevel.wl_surface())
{
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
// changes.
//
// FIXME: when unfullscreening to floating, this will send an extra configure with
// scrolling layout bounds. We should probably avoid it.
toplevel.send_configure();
mapped.set_needs_configure();
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -556,7 +566,7 @@ impl XdgShellHandler for State {
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -591,7 +601,7 @@ impl XdgShellHandler for State {
let configure_width = if is_floating {
*floating_width
} else if *is_full_width {
Some(ColumnWidth::Proportion(1.))
Some(PresetSize::Proportion(1.))
} else {
*width
};
@@ -642,13 +652,11 @@ impl XdgShellHandler for State {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: mapped.id().get(),
});
self.niri.stop_casts_for_target(CastTarget::Window {
id: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
@@ -678,7 +686,9 @@ impl XdgShellHandler for State {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
@@ -738,7 +748,13 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if toplevel.is_initial_configure_sent() {
toplevel.send_configure();
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
}
}
@@ -751,14 +767,20 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if toplevel.is_initial_configure_sent() {
toplevel.send_configure();
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
}
}
}
delegate_xdg_decoration!(State);
/// Whether KDE server decorations are in use.
#[derive(Default)]
#[derive(Default, Clone)]
pub struct KdeDecorationsModeState {
server: Cell<bool>,
}
@@ -862,7 +884,7 @@ impl State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
@@ -917,7 +939,7 @@ impl State {
let configure_width = if is_floating {
floating_width
} else if is_full_width {
Some(ColumnWidth::Proportion(1.))
Some(PresetSize::Proportion(1.))
} else {
width
};
@@ -931,16 +953,8 @@ impl State {
);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the tiled state for the initial configure.
update_tiled_state(toplevel, config.prefer_no_csd, rules.tiled_state);
// Set the configured settings.
*state = InitialConfigureState::Configured {
@@ -1051,6 +1065,19 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_size(output_geo.size);
// Background and bottom layer popups render below the top and the overlay layer, so let's
// put them into the non-exclusive zone.
//
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
// Smithay only computes the "all layers" non-exclusive zone atm.
//
// FIXME: related to the above, top layer popups should use the "overlay layer"
// non-exclusive zone.
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
target = map.non_exclusive_zone();
}
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
+51
View File
@@ -0,0 +1,51 @@
use ::input as libinput;
use smithay::backend::input;
use smithay::backend::winit::WinitVirtualDevice;
use smithay::output::Output;
use crate::niri::State;
use crate::protocols::virtual_pointer::VirtualPointer;
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
type NiriDevice: NiriInputDevice;
}
impl<T: input::InputBackend> NiriInputBackend for T
where
Self::Device: NiriInputDevice,
{
type NiriDevice = Self::Device;
}
pub trait NiriInputDevice: input::Device {
// FIXME: this should maybe be per-event, not per-device,
// but it's not clear that this matters in practice?
// it might be more obvious once we implement it for libinput
fn output(&self, state: &State) -> Option<Output>;
}
impl NiriInputDevice for libinput::Device {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: Allow specifying the output per-device?
None
}
}
impl NiriInputDevice for WinitVirtualDevice {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: we should be returning the single output that the winit backend creates,
// but for now, that will cause issues because the output is normally upside down,
// so we apply Transform::Flipped180 to it and that would also cause
// the cursor position to be flipped, which is not what we want.
//
// instead, we just return None and rely on the fact that it has only one output.
// doing so causes the cursor to be placed in *global* output coordinates,
// which are not flipped, and happen to be what we want.
None
}
}
impl NiriInputDevice for VirtualPointer {
fn output(&self, _: &State) -> Option<Output> {
self.output().cloned()
}
}
+954 -192
View File
File diff suppressed because it is too large Load Diff
+42 -36
View File
@@ -1,12 +1,11 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -17,16 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
is_moving: false,
gesture,
}
}
@@ -57,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
@@ -64,14 +97,6 @@ impl PointerGrab<State> for MoveGrab {
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
@@ -104,25 +129,6 @@ impl PointerGrab<State> for MoveGrab {
) {
handle.button(data, event);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// FIXME: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
+227
View File
@@ -0,0 +1,227 @@
use niri_ipc::PickedColor;
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::ButtonState;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
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};
pub struct PickColorGrab {
start_data: PointerGrabStartData<State>,
}
impl PickColorGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_color.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
state.niri.queue_redraw_all();
}
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
let (output, pos_within_output) = data.niri.output_under(location)?;
let output = output.clone();
data.backend
.with_primary_renderer(|renderer| {
data.niri.update_render_elements(Some(&output));
let scale = Scale::from(output.current_scale().fractional_scale());
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
// at the bottom/right edges of the monitors.
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::<i32, Physical>::from((1, 1));
let elements = data.niri.render(
renderer,
&output,
false,
// This is an interactive operation so we can render without blocking out.
RenderTarget::Output,
);
let pixels = match render_to_vec(
renderer,
size,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements.iter().rev().map(|elem| {
let offset = pos.upscale(-1);
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
}),
) {
Ok(pixels) => pixels,
Err(_) => return None,
};
if pixels.len() == 4 {
let rgb = [
f64::from(pixels[0]) / 255.0,
f64::from(pixels[1]) / 255.0,
f64::from(pixels[2]) / 255.0,
];
Some(PickedColor { rgb })
} else {
error!(
"unexpected pixel data length: {} (expected 4)",
pixels.len()
);
None
}
})
.flatten()
}
}
impl PointerGrab<State> for PickColorGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_color.take() {
let color = Self::pick_color_at_point(handle.current_location(), data);
let _ = tx.send_blocking(color);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+173
View File
@@ -0,0 +1,173 @@
use smithay::backend::input::ButtonState;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Point};
use crate::niri::State;
use crate::window::Mapped;
pub struct PickWindowGrab {
start_data: PointerGrabStartData<State>,
}
impl PickWindowGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_window.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
// Redraw to update the cursor.
state.niri.queue_redraw_all();
}
}
impl PointerGrab<State> for PickWindowGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_window.take() {
let _ = tx.send_blocking(
data.niri
.window_under(handle.current_location())
.map(Mapped::id),
);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+27 -5
View File
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
workspace_id: WorkspaceId,
gesture: GestureState,
}
@@ -27,12 +29,24 @@ enum GestureState {
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
workspace_id,
gesture,
}
}
@@ -81,8 +95,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
None
}
} else {
None
}
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
@@ -105,7 +127,7 @@ impl PointerGrab<State> for SpatialMovementGrab {
data.niri.queue_redraw(&output);
}
} else {
// The resize is no longer ongoing.
// The move is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
+39
View File
@@ -19,6 +19,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::PickWindow => Request::PickWindow,
Msg::PickColor => Request::PickColor,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
@@ -252,6 +254,43 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("No output is focused.");
}
}
Msg::PickWindow => {
let Response::PickedWindow(window) = response else {
bail!("unexpected response: expected PickedWindow, got {response:?}");
};
if json {
let window = serde_json::to_string(&window).context("error formatting response")?;
println!("{window}");
return Ok(());
}
if let Some(window) = window {
print_window(&window);
} else {
println!("No window selected.");
}
}
Msg::PickColor => {
let Response::PickedColor(color) = response else {
bail!("unexpected response: expected PickedColor, got {response:?}");
};
if json {
let color = serde_json::to_string(&color).context("error formatting response")?;
println!("{color}");
return Ok(());
}
if let Some(color) = color {
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
println!("Picked color: rgb({r}, {g}, {b})",);
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
} else {
println!("No color was picked.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
+77 -21
View File
@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::rc::Rc;
@@ -17,12 +18,17 @@ use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::desktop::layer_map_for_output;
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use crate::backend::IpcOutputMap;
use crate::input::pick_window_grab::PickWindowGrab;
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
use crate::utils::{version, with_toplevel_role};
@@ -33,7 +39,10 @@ use crate::window::Mapped;
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
pub struct IpcServer {
pub socket_path: PathBuf,
/// Path to the IPC socket.
///
/// This is `None` when creating `IpcServer` without a socket.
pub socket_path: Option<PathBuf>,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
@@ -60,31 +69,38 @@ struct EventStreamSender {
impl IpcServer {
pub fn start(
event_loop: &LoopHandle<'static, State>,
wayland_socket_name: &str,
wayland_socket_name: Option<&OsStr>,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("Ipc::start");
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
let wayland_socket_name = wayland_socket_name.to_string_lossy();
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
Ok(PostAction::Continue)
})
.unwrap();
Ok(PostAction::Continue)
})
.unwrap();
Some(socket_path)
} else {
None
};
Ok(Self {
socket_path,
@@ -119,7 +135,9 @@ impl IpcServer {
impl Drop for IpcServer {
fn drop(&mut self) {
let _ = unlink(&self.socket_path);
if let Some(socket_path) = &self.socket_path {
let _ = unlink(socket_path);
}
}
}
@@ -309,6 +327,44 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::PickWindow => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let pointer = state.niri.seat.get_pointer().unwrap();
let start_data = PointerGrabStartData {
focus: None,
button: 0,
location: pointer.current_location(),
};
let grab = PickWindowGrab::new(start_data);
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
// any.
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
state.niri.pick_window = Some(tx);
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
// Redraw to update the cursor.
state.niri.queue_redraw_all();
});
let result = rx.recv().await;
let id = result.map_err(|_| String::from("error getting picked window info"))?;
let window = id.and_then(|id| {
let state = ctx.event_stream_state.borrow();
state.windows.windows.get(&id.get()).cloned()
});
Response::PickedWindow(window)
}
Request::PickColor => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
state.handle_pick_color(tx);
});
let result = rx.recv().await;
let color = result.map_err(|_| String::from("error getting picked color"))?;
Response::PickedColor(color)
}
Request::Action(action) => {
let (tx, rx) = async_channel::bounded(1);
+52 -17
View File
@@ -1,16 +1,17 @@
use std::cell::RefCell;
use niri_config::layer_rule::LayerRule;
use niri_config::Config;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Rectangle, Scale};
use smithay::utils::{Logical, Point, Scale, Size};
use super::ResolvedLayerRules;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
@@ -23,25 +24,59 @@ pub struct MappedLayer {
rules: ResolvedLayerRules,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
block_out_buffer: SolidColorBuffer,
/// The shadow around the surface.
shadow: Shadow,
}
niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
}
}
impl MappedLayer {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules, config: &Config) -> Self {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = rules.shadow.resolve_against(shadow_config);
Self {
surface,
rules,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
shadow: Shadow::new(shadow_config),
}
}
pub fn update_config(&mut self, config: &Config) {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
self.shadow.update_config(shadow_config);
}
pub fn update_shaders(&mut self) {
self.shadow.update_shaders();
}
pub fn update_render_elements(&mut self, size: Size<f64, Logical>, scale: Scale<f64>) {
// Round to physical pixels.
let size = size.to_physical_precise_round(scale).to_logical(scale);
self.block_out_buffer.resize(size);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
// FIXME: is_active based on keyboard focus?
self.shadow
.update_render_elements(size, true, radius, scale.x, 1.);
}
pub fn surface(&self) -> &LayerSurface {
&self.surface
}
@@ -64,7 +99,7 @@ impl MappedLayer {
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
geometry: Rectangle<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
@@ -74,23 +109,19 @@ impl MappedLayer {
if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let geometry = geometry
.to_f64()
.to_physical_precise_round(scale)
.to_logical(scale);
let location = location.to_physical_precise_round(scale).to_logical(scale);
let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(geometry.size.to_f64());
// FIXME: take geometry-corner-radius into account.
let elem = SolidColorRenderElement::from_buffer(
&buffer,
geometry.loc,
&self.block_out_buffer,
location,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = geometry.loc;
let buf_pos = location;
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
@@ -100,7 +131,7 @@ impl MappedLayer {
rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset).to_physical_precise_round(scale),
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
@@ -117,6 +148,10 @@ impl MappedLayer {
);
}
let location = location.to_physical_precise_round(scale).to_logical(scale);
rv.normal
.extend(self.shadow.render(renderer, location).map(Into::into));
rv
}
}
+26 -3
View File
@@ -1,5 +1,5 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::BlockOutFrom;
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use smithay::desktop::LayerSurface;
pub mod mapped;
@@ -8,10 +8,17 @@ pub use mapped::MappedLayer;
/// Rules fully resolved for a layer-shell surface.
#[derive(Debug, PartialEq)]
pub struct ResolvedLayerRules {
/// Extra opacity to draw this window with.
/// Extra opacity to draw this layer surface with.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
/// Whether to block out this layer surface from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
/// Shadow overrides.
pub shadow: ShadowRule,
/// Corner radius to assume this layer surface has.
pub geometry_corner_radius: Option<CornerRadius>,
}
impl ResolvedLayerRules {
@@ -19,6 +26,17 @@ impl ResolvedLayerRules {
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,
}
}
@@ -52,6 +70,11 @@ impl ResolvedLayerRules {
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
if let Some(x) = rule.geometry_corner_radius {
resolved.geometry_corner_radius = Some(x);
}
resolved.shadow.merge_with(&rule.shadow);
}
resolved
+87 -58
View File
@@ -26,7 +26,7 @@ use crate::utils::{
use crate::window::ResolvedWindowRules;
/// By how many logical pixels the directional move commands move floating windows.
const DIRECTIONAL_MOVE_PX: f64 = 50.;
pub const DIRECTIONAL_MOVE_PX: f64 = 50.;
/// Space for floating windows.
#[derive(Debug)]
@@ -259,7 +259,16 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.tiles.iter().any(Tile::are_animations_ongoing) || !self.closing_windows.is_empty()
}
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
pub fn are_transitions_ongoing(&self) -> bool {
self.tiles.iter().any(Tile::are_transitions_ongoing) || !self.closing_windows.is_empty()
}
pub fn update_render_elements(
&mut self,
is_active: bool,
view_rect: Rectangle<f64, Logical>,
extra_scale: f64,
) {
let active = self.active_window_id.clone();
for (tile, offset) in self.tiles_with_offsets_mut() {
let id = tile.window().id();
@@ -267,7 +276,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
let mut tile_view_rect = view_rect;
tile_view_rect.loc -= offset + tile.render_offset();
tile.update(is_active, tile_view_rect);
tile.update_render_elements(is_active, tile_view_rect, extra_scale);
}
}
@@ -318,7 +327,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
})
}
pub fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
let border_config = rules.border.resolve_against(self.options.border);
compute_toplevel_bounds(border_config, self.working_area.size)
}
@@ -365,6 +374,14 @@ impl<W: LayoutElement> FloatingSpace<W> {
.map(Tile::window)
}
pub fn active_window_mut(&mut self) -> Option<&mut W> {
let id = self.active_window_id.as_ref()?;
self.tiles
.iter_mut()
.find(|tile| tile.window().id() == id)
.map(Tile::window_mut)
}
pub fn has_window(&self, id: &W::Id) -> bool {
self.tiles.iter().any(|tile| tile.window().id() == id)
}
@@ -615,7 +632,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
.preset_column_widths
.iter()
.position(|preset| {
let resolved = preset.resolve_no_gaps(&self.options, available_size);
let resolved = resolve_preset_size(*preset, available_size);
match resolved {
// Some allowance for fractional scaling purposes.
ResolvedSize::Tile(resolved) => current_tile + 1. < resolved,
@@ -626,19 +643,22 @@ impl<W: LayoutElement> FloatingSpace<W> {
};
let preset = self.options.preset_column_widths[preset_idx];
let change = match preset {
ColumnWidth::Proportion(prop) => SizeChange::SetProportion(prop * 100.),
ColumnWidth::Fixed(fixed) => SizeChange::SetFixed(fixed.round() as i32),
_ => unreachable!(),
};
self.set_window_width(Some(&id), change, true);
self.set_window_width(Some(&id), SizeChange::from(preset), true);
self.tiles[idx].floating_preset_width_idx = Some(preset_idx);
self.interactive_resize_end(Some(&id));
}
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
let Some(idx) = self.idx_of(id) else {
return false;
};
self.tiles[idx].start_open_animation();
true
}
pub fn toggle_window_height(&mut self, id: Option<&W::Id>) {
let Some(id) = id.or(self.active_window_id.as_ref()).cloned() else {
return;
@@ -669,12 +689,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
};
let preset = self.options.preset_window_heights[preset_idx];
let change = match preset {
PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.),
PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed),
};
self.set_window_height(Some(&id), change, true);
self.set_window_height(Some(&id), SizeChange::from(preset), true);
let tile = &mut self.tiles[idx];
tile.floating_preset_height_idx = Some(preset_idx);
@@ -836,6 +851,26 @@ impl<W: LayoutElement> FloatingSpace<W> {
}
}
pub fn focus_topmost(&mut self) {
let result = self
.tiles_with_offsets()
.min_by(|(_, pos_a), (_, pos_b)| f64::total_cmp(&pos_a.y, &pos_b.y));
if let Some((tile, _)) = result {
let id = tile.window().id().clone();
self.activate_window(&id);
}
}
pub fn focus_bottommost(&mut self) {
let result = self
.tiles_with_offsets()
.max_by(|(_, pos_a), (_, pos_b)| f64::total_cmp(&pos_a.y, &pos_b.y));
if let Some((tile, _)) = result {
let id = tile.window().id().clone();
self.activate_window(&id);
}
}
fn move_to(&mut self, idx: usize, new_pos: Point<f64, Logical>, animate: bool) {
if animate {
self.move_and_animate(idx, new_pos);
@@ -955,12 +990,13 @@ impl<W: LayoutElement> FloatingSpace<W> {
&self,
renderer: &mut R,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
focus_ring: bool,
) -> Vec<FloatingSpaceRenderElement<R>> {
let mut rv = Vec::new();
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.
@@ -975,7 +1011,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
rv.extend(
tile.render(renderer, tile_pos, scale, focus_ring, target)
tile.render(renderer, tile_pos, focus_ring, target)
.map(Into::into),
);
}
@@ -1135,53 +1171,34 @@ impl<W: LayoutElement> FloatingSpace<W> {
}
}
pub fn resolve_width(&self, width: ColumnWidth) -> ResolvedSize {
width.resolve_no_gaps(&self.options, self.working_area.size.w)
}
pub fn resolve_height(&self, height: PresetSize) -> ResolvedSize {
resolve_preset_size(height, self.working_area.size.h)
}
pub fn new_window_size(
&self,
width: Option<ColumnWidth>,
width: Option<PresetSize>,
height: Option<PresetSize>,
rules: &ResolvedWindowRules,
) -> Size<i32, Logical> {
let border = rules.border.resolve_against(self.options.border);
let width = if let Some(width) = width {
let width = match self.resolve_width(width) {
ResolvedSize::Tile(mut size) => {
if !border.off {
size -= border.width.0 * 2.;
let resolve = |size: Option<PresetSize>, working_area_size: f64| {
if let Some(size) = size {
let size = match resolve_preset_size(size, working_area_size) {
ResolvedSize::Tile(mut size) => {
if !border.off {
size -= border.width.0 * 2.;
}
size
}
size
}
ResolvedSize::Window(size) => size,
};
ResolvedSize::Window(size) => size,
};
max(1, width.floor() as i32)
} else {
0
max(1, size.floor() as i32)
} else {
0
}
};
let height = if let Some(height) = height {
let height = match self.resolve_height(height) {
ResolvedSize::Tile(mut size) => {
if !border.off {
size -= border.width.0 * 2.;
}
size
}
ResolvedSize::Window(size) => size,
};
max(1, height.floor() as i32)
} else {
0
};
let width = resolve(width, self.working_area.size.w);
let height = resolve(height, self.working_area.size.h);
Size::from((width, height))
}
@@ -1195,12 +1212,24 @@ impl<W: LayoutElement> FloatingSpace<W> {
let area = self.working_area;
let mut pos = Point::from((pos.x.0, pos.y.0));
if relative_to == RelativeTo::TopRight || relative_to == RelativeTo::BottomRight {
if relative_to == RelativeTo::TopRight
|| relative_to == RelativeTo::BottomRight
|| relative_to == RelativeTo::Right
{
pos.x = area.size.w - size.w - pos.x;
}
if relative_to == RelativeTo::BottomLeft || relative_to == RelativeTo::BottomRight {
if relative_to == RelativeTo::BottomLeft
|| relative_to == RelativeTo::BottomRight
|| relative_to == RelativeTo::Bottom
{
pos.y = area.size.h - size.h - pos.y;
}
if relative_to == RelativeTo::Top || relative_to == RelativeTo::Bottom {
pos.x += area.size.w / 2.0 - size.w / 2.0
}
if relative_to == RelativeTo::Left || relative_to == RelativeTo::Right {
pos.y += area.size.h / 2.0 - size.h / 2.0
}
pos + self.working_area.loc
})
+18 -11
View File
@@ -1,14 +1,15 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
use smithay::backend::renderer::element::Kind;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
use smithay::backend::renderer::element::{Element as _, Kind};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::utils::round_logical_in_physical_max1;
#[derive(Debug)]
pub struct FocusRing {
@@ -53,6 +54,7 @@ impl FocusRing {
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
@@ -61,8 +63,11 @@ impl FocusRing {
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let width = self.config.width.0;
// let scale = scale * extra_visual_scale;
// let width = self.config.width.0 / extra_visual_scale;
let width = round_logical_in_physical_max1(scale, self.config.width.0);
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
@@ -86,13 +91,7 @@ impl FocusRing {
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
let gradient_area = match gradient.relative_to {
@@ -187,6 +186,7 @@ impl FocusRing {
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
} else {
@@ -205,6 +205,7 @@ impl FocusRing {
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
@@ -235,7 +236,9 @@ impl FocusRing {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
let alpha = border.alpha();
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
.into()
};
rv.push(elem);
};
@@ -262,4 +265,8 @@ impl FocusRing {
pub fn is_off(&self) -> bool {
self.config.off
}
pub fn config(&self) -> &niri_config::FocusRing {
&self.config
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ impl InsertHintElement {
scale: f64,
) {
self.inner
.update_render_elements(size, true, false, view_rect, radius, scale);
.update_render_elements(size, true, false, view_rect, radius, scale, 1.);
}
pub fn render(
+1320 -3255
View File
File diff suppressed because it is too large Load Diff
+625 -247
View File
File diff suppressed because it is too large Load Diff
+30 -38
View File
@@ -2,32 +2,30 @@ use std::collections::HashMap;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
buffer: OffscreenBuffer,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
Shader = ShaderRenderElement,
}
}
@@ -37,6 +35,7 @@ impl OpenAnimation {
Self {
anim,
random_seed: fastrand::f32(),
buffer: OffscreenBuffer::default(),
}
}
@@ -55,23 +54,23 @@ impl OpenAnimation {
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
alpha: f32,
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
let (elem, _sync_point, mut data) = self
.buffer
.render(renderer, scale, elements)
.context("error rendering to offscreen buffer")?;
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
// can assume that below.
let offset = elem.offset();
let texture = elem.texture();
let texture_size = elem.logical_size();
let mut area = Rectangle::new(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
@@ -99,12 +98,12 @@ impl OpenAnimation {
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return Ok(ShaderRenderElement::new(
let elem = ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
alpha,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
@@ -116,36 +115,29 @@ impl OpenAnimation {
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc)
.into());
.with_location(area.loc);
// We're drawing the shader, not the offscreen itself.
data.id = elem.id().clone();
return Ok((elem.into(), data));
}
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
center.to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
(location + offset).to_physical_precise_round(scale),
location.to_physical_precise_round(scale),
Relocate::Relative,
);
Ok(elem.into())
Ok((elem.into(), data))
}
}
+1301 -275
View File
File diff suppressed because it is too large Load Diff
+184
View File
@@ -0,0 +1,184 @@
use std::iter::zip;
use niri_config::CornerRadius;
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
#[derive(Debug)]
pub struct Shadow {
shader_rects: Vec<Rectangle<f64, Logical>>,
shaders: Vec<ShadowRenderElement>,
config: niri_config::Shadow,
}
impl Shadow {
pub fn new(config: niri_config::Shadow) -> Self {
Self {
shader_rects: Vec::new(),
shaders: Vec::new(),
config,
}
}
pub fn update_config(&mut self, config: niri_config::Shadow) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size is rounded to physical pixels before being passed to this function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
let width = self.config.softness.0;
// Like in CSS box-shadow.
let sigma = width / 2.;
// Adjust width to draw all necessary pixels.
let width = ceil(sigma * 3.);
let offset = self.config.offset;
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
let spread = self.config.spread.0;
let spread = ceil(spread.abs()).copysign(spread);
let offset = offset - Point::from((spread, spread));
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
let box_size = if spread >= 0. {
win_size + Size::from((spread, spread)).upscale(2.)
} else {
// This is a saturating sub.
win_size - Size::from((-spread, -spread)).upscale(2.)
};
let radius = win_radius.expanded_by(spread as f32);
let shader_size = box_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.color
} else {
// Default to slightly more transparent.
self.config
.inactive_color
.unwrap_or(self.config.color * 0.75)
};
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
// This is actually offset relative to shader_geo, this is handled below.
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
if !self.config.draw_behind_window {
let top_left = ceil(f64::from(win_radius.top_left));
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
let bottom_left = f64::min(
win_size.h - top_left,
ceil(f64::from(win_radius.bottom_left)),
);
let bottom_right = f64::min(
win_size.h - top_right,
f64::min(
win_size.w - bottom_left,
ceil(f64::from(win_radius.bottom_right)),
),
);
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
let top_right = Rectangle::new(
Point::from((win_size.w - top_right, 0.)),
Size::from((top_right, top_right)),
);
let bottom_right = Rectangle::new(
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
Size::from((bottom_right, bottom_right)),
);
let bottom_left = Rectangle::new(
Point::from((0., win_size.h - bottom_left)),
Size::from((bottom_left, bottom_left)),
);
let mut background =
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
for rect in &mut background {
rect.loc -= offset;
}
self.shader_rects = shader_geo.subtract_rects(background);
self.shaders
.resize_with(self.shader_rects.len(), Default::default);
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
shader.update(
rect.size,
Rectangle::new(rect.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
win_radius,
alpha,
);
rect.loc += offset;
}
} else {
self.shader_rects.resize_with(1, Default::default);
self.shader_rects[0] = shader_geo;
self.shaders.resize_with(1, Default::default);
self.shaders[0].update(
shader_geo.size,
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::zero(),
Default::default(),
alpha,
);
self.shader_rects[0].loc += offset;
}
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
if !self.config.on {
return None.into_iter().flatten();
}
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
if !has_shadow_shader {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_rects)
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
Some(rv).into_iter().flatten()
}
}
+404
View File
@@ -0,0 +1,404 @@
use std::iter::zip;
use std::mem;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::tile::Tile;
use super::LayoutElement;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
};
#[derive(Debug)]
pub struct TabIndicator {
shader_locs: Vec<Point<f64, Logical>>,
shaders: Vec<BorderRenderElement>,
open_anim: Option<Animation>,
config: niri_config::TabIndicator,
}
#[derive(Debug)]
pub struct TabInfo {
/// Gradient for the tab indicator.
pub gradient: Gradient,
/// Tab geometry in the same coordinate system as the area.
pub geometry: Rectangle<f64, Logical>,
}
niri_render_elements! {
TabIndicatorRenderElement => {
Gradient = BorderRenderElement,
}
}
impl TabIndicator {
pub fn new(config: niri_config::TabIndicator) -> Self {
Self {
shader_locs: Vec::new(),
shaders: Vec::new(),
open_anim: None,
config,
}
}
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn advance_animations(&mut self) {
if let Some(anim) = &mut self.open_anim {
if anim.is_done() {
self.open_anim = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_anim.is_some()
}
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
}
fn tab_rects(
&self,
area: Rectangle<f64, Logical>,
count: usize,
scale: f64,
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let round = |logical: f64| round_logical_in_physical(scale, logical);
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
let width = round_max1(self.config.width.0);
let gap = round_max1(self.config.gap.0);
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position;
let side = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
};
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
let min_length = round(side * total_prop.clamp(0., 2.));
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
// growing and shrinking over the duration of the animation.
let pixel = 1. / scale;
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
let length = f64::max(min_length, shortest_length);
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
let px_per_tab = px_per_tab * progress;
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
match position {
TabIndicatorPosition::Left => (),
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
TabIndicatorPosition::Bottom => {
shader_loc.x = shader_loc.y;
shader_loc.y = area.size.h + gap;
}
}
shader_loc += area.loc;
(0..count).map(move |_| {
let mut px_per_tab = px_per_tab;
if ones_left > 0 {
ones_left -= 1;
px_per_tab += pixel;
}
let loc = shader_loc;
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
shader_loc.y += px_per_tab + gaps_between
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
shader_loc.x += px_per_tab + gaps_between
}
}
let size = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
Size::from((width, px_per_tab))
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
Size::from((px_per_tab, width))
}
};
Rectangle::new(loc, size)
})
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
enabled: bool,
// Geometry of the tabs area.
area: Rectangle<f64, Logical>,
// View rect relative to the tabs area.
area_view_rect: Rectangle<f64, Logical>,
// Tab count, should match the tabs iterator length.
tab_count: usize,
tabs: impl Iterator<Item = TabInfo>,
is_active: bool,
scale: f64,
) {
if !enabled || self.config.off {
self.shader_locs.clear();
self.shaders.clear();
return;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
self.shader_locs.clear();
self.shaders.clear();
return;
}
self.shaders.resize_with(count, Default::default);
self.shader_locs.resize_with(count, Default::default);
let position = self.config.position;
let radius = self.config.corner_radius.0 as f32;
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
let mut tabs_left = tab_count;
let rects = self.tab_rects(area, count, scale);
for ((shader, loc), (tab, rect)) in zip(
zip(&mut self.shaders, &mut self.shader_locs),
zip(tabs, rects),
) {
*loc = rect.loc;
let mut gradient_area = match tab.gradient.relative_to {
GradientRelativeTo::Window => tab.geometry,
GradientRelativeTo::WorkspaceView => area_view_rect,
};
gradient_area.loc -= *loc;
let mut color_from = tab.gradient.from;
let mut color_to = tab.gradient.to;
if !is_active {
color_from *= 0.5;
color_to *= 0.5;
}
let radius = if shared_rounded_corners && tab_count > 1 {
if tabs_left == tab_count {
// First tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: radius,
top_right: radius,
bottom_right: 0.,
bottom_left: 0.,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: radius,
top_right: 0.,
bottom_right: 0.,
bottom_left: radius,
},
}
} else if tabs_left == 1 {
// Last tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: 0.,
top_right: 0.,
bottom_right: radius,
bottom_left: radius,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: 0.,
top_right: radius,
bottom_right: radius,
bottom_left: 0.,
},
}
} else {
// Tab in the middle.
CornerRadius::default()
}
} else {
// Separate tabs, or the only tab.
CornerRadius::from(radius)
};
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
tabs_left -= 1;
shader.update(
rect.size,
gradient_area,
tab.gradient.in_,
color_from,
color_to,
((tab.gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_size(rect.size),
0.,
radius,
scale as f32,
1.,
);
}
}
pub fn hit(
&self,
area: Rectangle<f64, Logical>,
tab_count: usize,
scale: f64,
point: Point<f64, Logical>,
) -> Option<usize> {
if self.config.off {
return None;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
return None;
}
self.tab_rects(area, count, scale)
.enumerate()
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
pos: Point<f64, Logical>,
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
let has_border_shader = BorderRenderElement::has_shader(renderer);
if !has_border_shader {
return None.into_iter().flatten();
}
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()
}
/// Extra size occupied by the tab indicator.
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
if self.config.off
|| !self.config.place_within_column
|| (self.config.hide_when_single_tab && tab_count == 1)
{
return Size::from((0., 0.));
}
let round = |logical: f64| round_logical_in_physical(scale, logical);
let width = round(self.config.width.0);
let gap = round(self.config.gap.0);
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
// that it peeks from the other side of the window".
let size = f64::max(0., width + gap);
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
}
}
/// Offset of the tabbed content due to space occupied by the tab indicator.
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
self.extra_size(tab_count, scale).to_point()
}
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
}
}
pub fn config(&self) -> niri_config::TabIndicator {
self.config
}
}
impl TabInfo {
pub fn from_tile<W: LayoutElement>(
tile: &Tile<W>,
position: Point<f64, Logical>,
is_active: bool,
config: &niri_config::TabIndicator,
) -> Self {
let rules = tile.window().rules();
let rule = rules.tab_indicator;
let gradient_from_rule = || {
let (color, gradient) = if is_active {
(rule.active_color, rule.active_gradient)
} else {
(rule.inactive_color, rule.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_config = || {
let (color, gradient) = if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_border = || {
// Come up with tab indicator gradient matching the focus ring or the border, whichever
// one is enabled.
let focus_ring_config = tile.focus_ring().config();
let border_config = tile.border().config();
let config = if focus_ring_config.off {
border_config
} else {
focus_ring_config
};
let (color, gradient) = if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
gradient.unwrap_or_else(|| Gradient::from(color))
};
let gradient = gradient_from_rule()
.or_else(gradient_from_config)
.unwrap_or_else(gradient_from_border);
let geometry = Rectangle::new(position, tile.animated_tile_size());
TabInfo { gradient, geometry }
}
}
+3554
View File
File diff suppressed because it is too large Load Diff
+323 -95
View File
@@ -1,28 +1,32 @@
use core::f64;
use std::rc::Rc;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::shadow::Shadow;
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac,
RESIZE_ANIMATION_THRESHOLD,
HitType, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
SizeFrac, RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
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::{render_to_encompassing_texture, RenderTarget};
use crate::render_helpers::RenderTarget;
use crate::utils::transaction::Transaction;
use crate::utils::{round_logical_in_physical, round_logical_in_physical_max1};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -34,11 +38,11 @@ pub struct Tile<W: LayoutElement> {
border: FocusRing,
/// The focus ring around the window.
///
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
/// currently here.
focus_ring: FocusRing,
/// The shadow around the window.
shadow: Shadow,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
@@ -82,6 +86,9 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// The animation of the tile's opacity.
pub(super) alpha_animation: Option<AlphaAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
@@ -99,6 +106,11 @@ pub struct Tile<W: LayoutElement> {
/// Scale of the output the tile is on (and rounds its sizes to).
scale: f64,
/// Extra scale used for rendering.
///
/// Applied on top of `scale` and used for visuals only (does not affect the layout).
extra_overview_scale: f64,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -114,7 +126,9 @@ niri_render_elements! {
Opening = OpeningWindowRenderElement,
Resize = ResizeRenderElement,
Border = BorderRenderElement,
Shadow = ShadowRenderElement,
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
}
}
@@ -127,6 +141,7 @@ struct ResizeAnimation {
anim: Animation,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
offscreen: OffscreenBuffer,
}
#[derive(Debug)]
@@ -135,6 +150,18 @@ struct MoveAnimation {
from: f64,
}
#[derive(Debug)]
pub(super) struct AlphaAnimation {
pub(super) anim: Animation,
/// Whether the animation should persist after it's done.
///
/// This is used by things like interactive move which need to animate alpha to
/// semitransparent, then hold it at semitransparent for a while, until the operation
/// completes.
pub(super) hold_after_done: bool,
offscreen: OffscreenBuffer,
}
impl<W: LayoutElement> Tile<W> {
pub fn new(
window: W,
@@ -146,12 +173,14 @@ impl<W: LayoutElement> Tile<W> {
let rules = window.rules();
let border_config = rules.border.resolve_against(options.border);
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
let shadow_config = rules.shadow.resolve_against(options.shadow);
let is_fullscreen = window.is_fullscreen();
Self {
window,
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
shadow: Shadow::new(shadow_config),
is_fullscreen,
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
unfullscreen_to_floating: false,
@@ -163,11 +192,13 @@ impl<W: LayoutElement> Tile<W> {
resize_animation: None,
move_x_animation: None,
move_y_animation: None,
alpha_animation: None,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
view_size,
scale,
extra_overview_scale: 1.,
clock,
options,
}
@@ -201,19 +232,23 @@ impl<W: LayoutElement> Tile<W> {
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
self.fullscreen_backdrop.resize(view_size);
}
pub fn update_shaders(&mut self) {
self.border.update_shaders();
self.focus_ring.update_shaders();
self.shadow.update_shaders();
}
pub fn update_window(&mut self) {
self.is_fullscreen = self.window.is_fullscreen();
if let Some(animate_from) = self.window.take_animation_snapshot() {
let size_from = if let Some(resize) = self.resize_animation.take() {
let (size_from, offscreen) = if let Some(resize) = self.resize_animation.take() {
// Compute like in animated_window_size(), but using the snapshot geometry (since
// the current one is already overwritten).
let mut size = animate_from.size;
@@ -224,9 +259,10 @@ impl<W: LayoutElement> Tile<W> {
size.w = size_from.w + (size.w - size_from.w) * val;
size.h = size_from.h + (size.h - size_from.h) * val;
size
// Also try to reuse the existing offscreen buffer if we have one.
(size, resize.offscreen)
} else {
animate_from.size
(animate_from.size, OffscreenBuffer::default())
};
let change = self.window.size().to_f64().to_point() - size_from.to_point();
@@ -243,6 +279,7 @@ impl<W: LayoutElement> Tile<W> {
anim,
size_from,
snapshot: animate_from,
offscreen,
});
} else {
self.resize_animation = None;
@@ -257,6 +294,9 @@ impl<W: LayoutElement> Tile<W> {
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
@@ -289,18 +329,40 @@ impl<W: LayoutElement> Tile<W> {
self.move_y_animation = None;
}
}
if let Some(alpha) = &mut self.alpha_animation {
if !alpha.hold_after_done && alpha.anim.is_done() {
self.alpha_animation = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.are_transitions_ongoing() || self.window.rules().baba_is_float == Some(true)
}
pub fn are_transitions_ongoing(&self) -> bool {
self.open_animation.is_some()
|| self.resize_animation.is_some()
|| self.move_x_animation.is_some()
|| self.move_y_animation.is_some()
|| self
.alpha_animation
.as_ref()
.is_some_and(|alpha| !alpha.anim.is_done())
}
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
pub fn update_render_elements(
&mut self,
is_active: bool,
view_rect: Rectangle<f64, Logical>,
extra_overview_scale: f64,
) {
let rules = self.window.rules();
self.extra_overview_scale = extra_overview_scale;
let visual_scale = self.scale * extra_overview_scale;
let draw_border_with_background = rules
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
@@ -323,7 +385,23 @@ impl<W: LayoutElement> Tile<W> {
view_rect.size,
),
radius,
self.scale,
visual_scale,
1.,
);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
};
self.shadow.update_render_elements(
self.animated_tile_size(),
is_active,
radius,
visual_scale,
1.,
);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@@ -331,21 +409,15 @@ impl<W: LayoutElement> Tile<W> {
} else {
draw_border_with_background
};
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
}
.expanded_by(self.focus_ring.width() as f32);
let radius = radius.expanded_by(self.focus_ring.width() as f32);
self.focus_ring.update_render_elements(
self.animated_tile_size(),
is_active,
!draw_focus_ring_with_background,
view_rect,
radius,
self.scale,
visual_scale,
1.,
);
}
@@ -430,6 +502,39 @@ impl<W: LayoutElement> Tile<W> {
self.move_y_animation = None;
}
pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) {
let from = from.clamp(0., 1.);
let to = to.clamp(0., 1.);
let (current, offscreen) = if let Some(alpha) = self.alpha_animation.take() {
(alpha.anim.clamped_value(), alpha.offscreen)
} else {
(from, OffscreenBuffer::default())
};
self.alpha_animation = Some(AlphaAnimation {
anim: Animation::new(self.clock.clone(), current, to, 0., config),
hold_after_done: false,
offscreen,
});
}
pub fn ensure_alpha_animates_to_1(&mut self) {
if let Some(alpha) = &self.alpha_animation {
if alpha.anim.to() != 1. {
// Cancel animation instead of starting a new one because the user likely wants to
// see the tile right away.
self.alpha_animation = None;
}
}
}
pub fn hold_alpha_animation_after_done(&mut self) {
if let Some(alpha) = &mut self.alpha_animation {
alpha.hold_after_done = true;
}
}
pub fn window(&self) -> &W {
&self.window
}
@@ -455,6 +560,12 @@ impl<W: LayoutElement> Tile<W> {
Some(self.border.width())
}
pub fn visual_effective_border_width(&self) -> Option<f64> {
let visual_scale = self.scale * self.extra_overview_scale;
self.effective_border_width()
.map(move |w| round_logical_in_physical_max1(visual_scale, w))
}
/// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
@@ -486,6 +597,38 @@ impl<W: LayoutElement> Tile<W> {
loc
}
pub fn visual_window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
let visual_scale = self.scale * self.extra_overview_scale;
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window_size();
let target_size = self.view_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
if window_size.w < target_size.w {
loc.x += (target_size.w - window_size.w) / 2.;
}
if window_size.h < target_size.h {
loc.y += (target_size.h - window_size.h) / 2.;
}
// Round to physical pixels.
loc = loc
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
}
if let Some(width) = self.visual_effective_border_width() {
loc += (width, width).into();
}
loc
}
pub fn tile_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
@@ -541,9 +684,11 @@ impl<W: LayoutElement> Tile<W> {
size
}
fn animated_window_size(&self) -> Size<f64, Logical> {
pub fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if let Some(resize) = &self.resize_animation {
let val = resize.anim.value();
let size_from = resize.size_from.to_f64();
@@ -551,25 +696,30 @@ impl<W: LayoutElement> Tile<W> {
size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val);
size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val);
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
}
size
}
fn animated_tile_size(&self) -> Size<f64, Logical> {
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
size = size
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
return size;
}
if let Some(width) = self.effective_border_width() {
if let Some(width) = self.visual_effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
@@ -584,16 +734,32 @@ impl<W: LayoutElement> Tile<W> {
loc
}
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
point -= self.window_loc().to_f64();
self.window.is_in_input_region(point)
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_size(self.tile_size());
activation_region.contains(point)
}
pub fn hit(&self, point: Point<f64, Logical>) -> Option<HitType> {
let offset = self.bob_offset();
let point = point - offset;
if self.is_in_input_region(point) {
let win_pos = self.buf_loc() + offset;
Some(HitType::Input { win_pos })
} else if self.is_in_activation_region(point) {
Some(HitType::Activate {
is_tab_indicator: false,
})
} else {
None
}
}
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
@@ -611,7 +777,7 @@ impl<W: LayoutElement> Tile<W> {
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window
.request_size(size.to_i32_floor(), animate, transaction);
.request_size(size.to_i32_floor(), false, animate, transaction);
}
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
@@ -646,15 +812,18 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn request_fullscreen(&mut self) {
pub fn request_fullscreen(&mut self, animate: bool, transaction: Option<Transaction>) {
self.window
.request_fullscreen(self.view_size.to_i32_round());
.request_size(self.view_size.to_i32_round(), true, animate, transaction);
}
pub fn min_size(&self) -> Size<f64, Logical> {
pub fn min_size_nonfullscreen(&self) -> Size<f64, Logical> {
let mut size = self.window.min_size().to_f64();
if let Some(width) = self.effective_border_width() {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
size.w = f64::max(1., size.w);
size.h = f64::max(1., size.h);
@@ -665,10 +834,13 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn max_size(&self) -> Size<f64, Logical> {
pub fn max_size_nonfullscreen(&self) -> Size<f64, Logical> {
let mut size = self.window.max_size().to_f64();
if let Some(width) = self.effective_border_width() {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
if size.w > 0. {
size.w += width * 2.;
}
@@ -680,6 +852,20 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if self.window.rules().baba_is_float != Some(true) {
return Point::from((0., 0.));
}
let visual_scale = self.scale * self.extra_overview_scale;
let now = self.clock.now().as_secs_f64();
let amplitude = self.view_size.h / 96.;
let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.);
let y = round_logical_in_physical(visual_scale, y);
Point::from((0., y))
}
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
@@ -691,23 +877,35 @@ impl<W: LayoutElement> Tile<W> {
.unwrap_or_else(|| !self.window.has_ssd())
}
fn render_inner<R: NiriRenderer>(
&self,
fn render_inner<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render_inner");
let alpha = if self.is_fullscreen {
let scale = Scale::from(self.scale);
let visual_scale = scale * self.extra_overview_scale;
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
let window_loc = self.window_loc();
// This is here rather than in render_offset() because render_offset() is currently assumed
// by the code to be temporary. So, for example, interactive move will try to "grab" the
// tile at its current render offset and reset the render offset to zero by cancelling the
// tile move animations. On the other hand, bob_offset() is not resettable, so adding it in
// render_offset() would cause obvious animation glitches.
//
// 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 window_loc = self.visual_window_loc();
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
@@ -725,7 +923,7 @@ impl<W: LayoutElement> Tile<W> {
if let Some(resize) = &self.resize_animation {
resize_popups = Some(
self.window
.render_popups(renderer, window_render_loc, scale, alpha, target)
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
.into_iter()
.map(Into::into),
);
@@ -742,15 +940,11 @@ impl<W: LayoutElement> Tile<W> {
target,
);
let current = render_to_encompassing_texture(
gles_renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&window_elements,
)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
let current = resize
.offscreen
.render(gles_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.
@@ -763,7 +957,13 @@ impl<W: LayoutElement> Tile<W> {
clip_to_geometry
};
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
// The offset and size are computed in physical pixels and converted to
// logical with the same `scale`, so converting them back with rounding
// inside the geometry() call gives us the same physical result back.
let texture_current_geo = elem_current.geometry(scale);
let elem = ResizeRenderElement::new(
area,
scale,
@@ -775,12 +975,15 @@ impl<W: LayoutElement> Tile<W> {
resize.anim.clamped_value().clamp(0., 1.) as f32,
radius,
clip_to_geometry,
alpha,
win_alpha,
);
// FIXME: with split popups, this will use the resize element ID for
// popups, but we want the real IDs.
self.window
.set_offscreen_element_id(Some(elem.id().clone()));
// We're drawing the resize shader, not the offscreen directly.
data.id = elem.id().clone();
// 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());
}
}
@@ -792,12 +995,11 @@ impl<W: LayoutElement> Tile<W> {
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc,
alpha,
win_alpha,
Kind::Unspecified,
)
.into(),
);
self.window.set_offscreen_element_id(None);
}
}
@@ -808,7 +1010,7 @@ impl<W: LayoutElement> Tile<W> {
if resize_shader.is_none() && resize_fallback.is_none() {
let window = self
.window
.render(renderer, window_render_loc, scale, alpha, target);
.render(renderer, window_render_loc, scale, win_alpha, target);
let geo = Rectangle::new(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
@@ -861,6 +1063,7 @@ impl<W: LayoutElement> Tile<W> {
0.,
radius,
scale.x as f32,
1.,
)
.with_location(geo.loc)
.into();
@@ -894,7 +1097,7 @@ impl<W: LayoutElement> Tile<W> {
});
let rv = rv.chain(elem);
let elem = self.effective_border_width().map(|width| {
let elem = self.visual_effective_border_width().map(|width| {
self.border
.render(renderer, location + Point::from((width, width)))
.map(Into::into)
@@ -902,81 +1105,98 @@ impl<W: LayoutElement> Tile<W> {
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
rv.chain(elem.into_iter().flatten())
let rv = rv.chain(elem.into_iter().flatten());
rv.chain(self.shadow.render(renderer, location).map(Into::into))
}
pub fn render<R: NiriRenderer>(
&self,
pub fn render<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render");
let scale = Scale::from(self.scale);
let tile_alpha = self
.alpha_animation
.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;
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.)), scale, focus_ring, target);
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match open.render(renderer, &elements, self.tile_size(), location, scale) {
Ok(elem) => {
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
match open.render(
renderer,
&elements,
self.animated_tile_size(),
location,
scale,
tile_alpha,
) {
Ok((elem, data)) => {
self.window().set_offscreen_data(Some(data));
open_anim_elem = Some(elem.into());
}
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) {
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());
}
Err(err) => {
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
}
}
}
if open_anim_elem.is_none() {
self.window().set_offscreen_element_id(None);
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
}
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,
scale: Scale<f64>,
) {
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
self.unmap_snapshot = Some(self.render_snapshot(renderer));
}
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) -> TileRenderSnapshot {
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
);
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,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Screencast,
);
@@ -995,6 +1215,14 @@ impl<W: LayoutElement> Tile<W> {
self.unmap_snapshot.take()
}
pub fn border(&self) -> &FocusRing {
&self.border
}
pub fn focus_ring(&self) -> &FocusRing {
&self.focus_ring
}
pub fn options(&self) -> &Rc<Options> {
&self.options
}
+239 -56
View File
@@ -2,14 +2,17 @@ use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
use niri_ipc::{PositionChange, SizeChange};
use niri_config::{
CenterFocusedColumn, CornerRadius, FloatOrInt, OutputName, PresetSize,
Workspace as WorkspaceConfig,
};
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use smithay::utils::{Logical, Point, Rectangle, Serial, Size, Transform};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::SurfaceCachedState;
@@ -18,11 +21,15 @@ use super::scrolling::{
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
ScrollingSpaceRenderElement,
};
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot};
use super::{ActivateWindow, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac};
use super::{
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
};
use crate::animation::Clock;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -78,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
/// zones.
working_area: Rectangle<f64, Logical>,
/// This workspace's shadow in the overview.
shadow: Shadow,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -226,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self {
scrolling,
floating,
@@ -235,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
transform: output.current_transform(),
view_size,
working_area,
shadow: Shadow::new(shadow_config),
output: Some(output),
clock,
base_options,
@@ -279,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self {
scrolling,
floating,
@@ -289,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
original_output,
view_size,
working_area,
shadow: Shadow::new(shadow_config),
clock,
base_options,
options,
@@ -331,16 +365,37 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn are_transitions_ongoing(&self) -> bool {
self.scrolling.are_transitions_ongoing() || self.floating.are_animations_ongoing()
self.scrolling.are_transitions_ongoing() || self.floating.are_transitions_ongoing()
}
pub fn update_render_elements(&mut self, is_active: bool) {
self.scrolling
.update_render_elements(is_active && !self.floating_is_active.get());
pub fn update_render_elements(
&mut self,
is_active: bool,
is_overview_open: bool,
extra_overview_scale: f64,
) {
self.scrolling.update_render_elements(
is_active && !self.floating_is_active.get(),
is_overview_open,
extra_overview_scale,
);
let view_rect = Rectangle::from_size(self.view_size);
self.floating
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
self.floating.update_render_elements(
is_active && self.floating_is_active.get(),
view_rect,
extra_overview_scale,
);
let visual_scale = self.scale.fractional_scale() * extra_overview_scale;
self.shadow.update_render_elements(
self.view_size,
true,
CornerRadius::default(),
visual_scale,
1.,
);
}
pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -368,6 +423,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_shaders(&mut self) {
self.scrolling.update_shaders();
self.floating.update_shaders();
self.shadow.update_shaders();
}
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
@@ -406,6 +462,14 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn active_window_mut(&mut self) -> Option<&mut W> {
if self.floating_is_active.get() {
self.floating.active_window_mut()
} else {
self.scrolling.active_window_mut()
}
}
pub fn is_active_fullscreen(&self) -> bool {
self.scrolling.is_active_fullscreen()
}
@@ -567,10 +631,10 @@ impl<W: LayoutElement> Workspace<W> {
self.floating.add_tile_above(next_to, tile, activate);
} else {
// FIXME: use static pos
let (next_to_tile, render_pos) = self
let (next_to_tile, render_pos, _visible) = self
.scrolling
.tiles_with_render_positions()
.find(|(tile, _)| tile.window().id() == next_to)
.find(|(tile, _, _)| tile.window().id() == next_to)
.unwrap();
// Position the new tile in the center above the next_to tile. Think a
@@ -705,9 +769,9 @@ impl<W: LayoutElement> Workspace<W> {
pub fn resolve_default_width(
&self,
default_width: Option<Option<ColumnWidth>>,
default_width: Option<Option<PresetSize>>,
is_floating: bool,
) -> Option<ColumnWidth> {
) -> Option<PresetSize> {
match default_width {
Some(Some(width)) => Some(width),
Some(None) => None,
@@ -732,7 +796,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn new_window_size(
&self,
width: Option<ColumnWidth>,
width: Option<PresetSize>,
height: Option<PresetSize>,
is_floating: bool,
rules: &ResolvedWindowRules,
@@ -764,7 +828,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn configure_new_window(
&self,
window: &Window,
width: Option<ColumnWidth>,
width: Option<PresetSize>,
height: Option<PresetSize>,
is_floating: bool,
rules: &ResolvedWindowRules,
@@ -789,9 +853,9 @@ impl<W: LayoutElement> Workspace<W> {
}
if is_floating {
state.bounds = Some(self.floating.toplevel_bounds(rules));
state.bounds = Some(self.floating.new_window_toplevel_bounds(rules));
} else {
state.bounds = Some(self.scrolling.toplevel_bounds(rules));
state.bounds = Some(self.scrolling.new_window_toplevel_bounds(rules));
}
});
}
@@ -840,6 +904,20 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn focus_column(&mut self, index: usize) {
if self.floating_is_active.get() {
self.focus_tiling();
}
self.scrolling.focus_column(index);
}
pub fn focus_window_in_column(&mut self, index: u8) {
if self.floating_is_active.get() {
return;
}
self.scrolling.focus_window_in_column(index);
}
pub fn focus_down(&mut self) -> bool {
if self.floating_is_active.get() {
self.floating.focus_down()
@@ -888,6 +966,34 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn focus_window_top(&mut self) {
if self.floating_is_active.get() {
self.floating.focus_topmost();
} else {
self.scrolling.focus_top();
}
}
pub fn focus_window_bottom(&mut self) {
if self.floating_is_active.get() {
self.floating.focus_bottommost();
} else {
self.scrolling.focus_bottom();
}
}
pub fn focus_window_down_or_top(&mut self) {
if !self.focus_down() {
self.focus_window_top();
}
}
pub fn focus_window_up_or_bottom(&mut self) {
if !self.focus_up() {
self.focus_window_bottom();
}
}
pub fn move_left(&mut self) -> bool {
if self.floating_is_active.get() {
self.floating.move_left();
@@ -920,6 +1026,13 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.move_column_to_last();
}
pub fn move_column_to_index(&mut self, index: usize) {
if self.floating_is_active.get() {
return;
}
self.scrolling.move_column_to_index(index);
}
pub fn move_down(&mut self) -> bool {
if self.floating_is_active.get() {
self.floating.move_down();
@@ -977,6 +1090,20 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.swap_window_in_direction(direction);
}
pub fn toggle_column_tabbed_display(&mut self) {
if self.floating_is_active.get() {
return;
}
self.scrolling.toggle_column_tabbed_display();
}
pub fn set_column_display(&mut self, display: ColumnDisplay) {
if self.floating_is_active.get() {
return;
}
self.scrolling.set_column_display(display);
}
pub fn center_column(&mut self) {
if self.floating_is_active.get() {
self.floating.center_window(None);
@@ -1069,6 +1196,13 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn expand_column_to_available_width(&mut self) {
if self.floating_is_active.get() {
return;
}
self.scrolling.expand_column_to_available_width();
}
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
let mut unfullscreen_to_floating = false;
if self.floating.has_window(window) {
@@ -1291,7 +1425,6 @@ impl<W: LayoutElement> Workspace<W> {
&self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
let scrolling = self.scrolling.tiles_with_render_positions();
let scrolling = scrolling.map(|(tile, pos)| (tile, pos, true));
let floating = self.floating.tiles_with_render_positions();
let visible = self.is_floating_visible();
@@ -1330,30 +1463,36 @@ impl<W: LayoutElement> Workspace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
is_overview_open: bool,
) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
let scale = Scale::from(self.scale.fractional_scale());
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
let scrolling =
self.scrolling
.render_elements(renderer, scale, target, scrolling_focus_ring);
let scrolling = self.scrolling.render_elements(
renderer,
target,
scrolling_focus_ring,
is_overview_open,
);
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
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,
scale,
target,
floating_focus_ring,
);
let floating =
self.floating
.render_elements(renderer, view_rect, target, floating_focus_ring);
floating.into_iter().map(WorkspaceRenderElement::from)
});
floating.into_iter().flatten().chain(scrolling)
}
pub fn render_shadow<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
self.shadow.render(renderer, Point::from((0., 0.)))
}
pub fn render_above_top_layer(&self) -> bool {
self.scrolling.render_above_top_layer()
}
@@ -1367,14 +1506,13 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
let output_scale = Scale::from(self.scale.fractional_scale());
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(false, view_rect);
tile.store_unmap_snapshot_if_empty(renderer, output_scale);
tile.update_render_elements(false, view_rect, 1.);
tile.store_unmap_snapshot_if_empty(renderer);
return;
}
}
@@ -1416,27 +1554,23 @@ impl<W: LayoutElement> Workspace<W> {
.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
}
pub fn window_under(
&self,
pos: Point<f64, Logical>,
) -> Option<(&W, Option<Point<f64, Logical>>)> {
self.tiles_with_render_positions()
.find_map(|(tile, tile_pos, visible)| {
if !visible {
return None;
}
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
self.scrolling.start_open_animation(id) || self.floating.start_open_animation(id)
}
let pos_within_tile = pos - tile_pos;
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
// This logic is consistent with tiles_with_render_positions().
if self.is_floating_visible() {
if let Some(rv) = self
.floating
.tiles_with_render_positions()
.find_map(|(tile, tile_pos)| HitType::hit_tile(tile, tile_pos, pos))
{
return Some(rv);
}
}
if tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + tile.buf_loc();
return Some((tile.window(), Some(pos_within_surface)));
} else if tile.is_in_activation_region(pos_within_tile) {
return Some((tile.window(), None));
}
None
})
self.scrolling.window_under(pos)
}
pub fn resize_edges_under(&self, pos: Point<f64, Logical>) -> Option<ResizeEdge> {
@@ -1450,9 +1584,7 @@ impl<W: LayoutElement> Workspace<W> {
let pos_within_tile = pos - tile_pos;
if tile.is_in_input_region(pos_within_tile)
|| tile.is_in_activation_region(pos_within_tile)
{
if tile.hit(pos_within_tile).is_some() {
let size = tile.tile_size().to_f64();
let mut edges = ResizeEdge::empty();
@@ -1557,6 +1689,45 @@ impl<W: LayoutElement> Workspace<W> {
.view_offset_gesture_end(cancelled, is_touchpad)
}
pub fn dnd_scroll_gesture_begin(&mut self) {
self.scrolling.dnd_scroll_gesture_begin();
}
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let config = &self.options.gestures.dnd_edge_view_scroll;
let trigger_width = config.trigger_width.0;
// This working area intentionally does not include extra struts from Options.
let x = pos.x - self.working_area.loc.x;
let width = self.working_area.size.w;
let x = x.clamp(0., width);
let trigger_width = trigger_width.clamp(0., width / 2.);
let delta = if x < trigger_width {
-(trigger_width - x)
} else if width - x < trigger_width {
trigger_width - (width - x)
} else {
0.
};
let delta = if trigger_width < 0.01 {
// Sanity check for trigger-width 0 or small window sizes.
0.
} else {
// Normalize to [0, 1].
delta / trigger_width
};
let delta = delta * speed;
self.scrolling.dnd_scroll_gesture_scroll(delta)
}
pub fn dnd_scroll_gesture_end(&mut self) {
self.scrolling.dnd_scroll_gesture_end();
}
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
if self.floating.has_window(&window) {
self.floating.interactive_resize_begin(window, edges)
@@ -1644,7 +1815,7 @@ impl<W: LayoutElement> Workspace<W> {
);
}
for (tile, tile_pos, _visible) in self.tiles_with_render_positions() {
for (tile, tile_pos, visible) in self.tiles_with_render_positions() {
if Some(tile.window().id()) != move_win_id {
assert_eq!(tile.interactive_move_offset, Point::from((0., 0.)));
}
@@ -1654,6 +1825,18 @@ impl<W: LayoutElement> Workspace<W> {
// Tile positions must be rounded to physical pixels.
assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5);
assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5);
if let Some(alpha) = &tile.alpha_animation {
let anim = &alpha.anim;
if visible {
assert_eq!(anim.to(), 1., "visible tiles can animate alpha only to 1");
}
assert!(
!alpha.hold_after_done,
"tiles in the layout cannot have held alpha animation"
);
}
}
}
}
+36 -20
View File
@@ -5,11 +5,11 @@ use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, mem};
use clap::Parser;
use clap::{CommandFactory, Parser};
use directories::ProjectDirs;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
@@ -48,6 +48,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_writer(io::stderr)
.with_env_filter(env_filter)
.init();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
@@ -58,19 +66,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let cli = Cli::parse();
if cli.session {
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
// the winit backend to be selected instead.
// DISPLAY, WAYLAND_DISPLAY or WAYLAND_SOCKET from our environment if they are set, since
// they will cause the winit backend to be selected instead.
if env::var_os("DISPLAY").is_some() {
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
@@ -79,6 +80,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
if env::var_os("WAYLAND_SOCKET").is_some() {
warn!("running as a session but WAYLAND_SOCKET is set, removing it");
env::remove_var("WAYLAND_SOCKET");
}
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
@@ -86,9 +91,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("XDG_SESSION_TYPE", "wayland");
}
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
@@ -105,6 +107,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
Sub::Panic => cause_panic(),
Sub::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "niri", &mut io::stdout());
return Ok(());
}
}
}
@@ -177,11 +183,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.get_signal(),
display,
false,
true,
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
let socket_name = state.niri.socket_name.as_deref().unwrap();
env::set_var("WAYLAND_DISPLAY", socket_name);
info!(
"listening on Wayland socket: {}",
@@ -190,8 +197,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
let socket_path = ipc.socket_path.as_deref().unwrap();
env::set_var(SOCKET_PATH_ENV, socket_path);
info!("IPC listening on: {}", socket_path.to_string_lossy());
}
if cli.session {
@@ -224,12 +232,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up config file watcher.
let _watcher = {
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
// watcher thread.
let process = |path: &Path| {
Config::load(path).map_err(|err| {
warn!("{:?}", err.context("error loading config"));
})
};
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(watch_path.clone(), tx);
let watcher = Watcher::new(watch_path.clone(), process, tx);
event_loop
.handle()
.insert_source(rx, move |event, _, state| match event {
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(config) => state.reload_config(config),
calloop::channel::Event::Closed => (),
})
.unwrap();
+1130 -434
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -3,5 +3,6 @@ pub mod gamma_control;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
pub mod virtual_pointer;
pub mod raw;
+563
View File
@@ -0,0 +1,563 @@
use std::collections::HashSet;
use std::sync::Mutex;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device,
DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent,
PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent,
};
use smithay::input::pointer::AxisFrame;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::protocol::wl_pointer;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use wayland_backend::protocol::WEnum;
use wayland_protocols_wlr::virtual_pointer::v1::server::{
zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1,
};
use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1;
use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1;
const VERSION: u32 = 2;
pub struct VirtualPointerManagerState {
virtual_pointers: HashSet<ZwlrVirtualPointerV1>,
}
pub struct VirtualPointerManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub struct VirtualPointerInputBackend;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct VirtualPointer {
pointer: ZwlrVirtualPointerV1,
}
#[derive(Debug)]
pub struct VirtualPointerUserData {
seat: Option<WlSeat>,
output: Option<Output>,
axis_frame: Mutex<Option<AxisFrame>>,
}
impl VirtualPointer {
fn data(&self) -> &VirtualPointerUserData {
self.pointer.data().unwrap()
}
pub fn seat(&self) -> Option<&WlSeat> {
self.data().seat.as_ref()
}
pub fn output(&self) -> Option<&Output> {
self.data().output.as_ref()
}
fn finish_axis_frame(&self) -> Option<AxisFrame> {
self.data().axis_frame.lock().unwrap().take()
}
fn mutate_axis_frame(&self, time: Option<u32>, f: impl FnOnce(AxisFrame) -> AxisFrame) {
let mut frame = self.data().axis_frame.lock().unwrap();
*frame = frame.or(time.map(AxisFrame::new)).map(f);
}
}
impl Device for VirtualPointer {
fn id(&self) -> String {
format!("wlr virtual pointer {}", self.pointer.id())
}
fn name(&self) -> String {
String::from("virtual pointer")
}
fn has_capability(&self, capability: DeviceCapability) -> bool {
matches!(capability, DeviceCapability::Pointer)
}
fn usb_id(&self) -> Option<(u32, u32)> {
None
}
fn syspath(&self) -> Option<std::path::PathBuf> {
None
}
}
pub struct VirtualPointerMotionEvent {
pointer: VirtualPointer,
time: u32,
dx: f64,
dy: f64,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl PointerMotionEvent<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
fn delta_x(&self) -> f64 {
self.dx
}
fn delta_y(&self) -> f64 {
self.dy
}
fn delta_x_unaccel(&self) -> f64 {
self.dx
}
fn delta_y_unaccel(&self) -> f64 {
self.dy
}
}
pub struct VirtualPointerMotionAbsoluteEvent {
pointer: VirtualPointer,
time: u32,
x: u32,
y: u32,
x_extent: u32,
y_extent: u32,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl AbsolutePositionEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
fn x(&self) -> f64 {
self.x as f64 / self.x_extent as f64
}
fn y(&self) -> f64 {
self.y as f64 / self.y_extent as f64
}
fn x_transformed(&self, width: i32) -> f64 {
(self.x as i64 * width as i64) as f64 / self.x_extent as f64
}
fn y_transformed(&self, height: i32) -> f64 {
(self.y as i64 * height as i64) as f64 / self.y_extent as f64
}
}
pub struct VirtualPointerButtonEvent {
pointer: VirtualPointer,
time: u32,
button: u32,
state: ButtonState,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
impl PointerButtonEvent<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
fn button_code(&self) -> u32 {
self.button
}
fn state(&self) -> ButtonState {
self.state
}
}
pub struct VirtualPointerAxisEvent {
pointer: VirtualPointer,
frame: AxisFrame,
}
impl Event<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
fn time(&self) -> u64 {
self.frame.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualPointer {
self.pointer.clone()
}
}
fn tuple_axis<T>(tuple: (T, T), axis: Axis) -> T {
match axis {
Axis::Horizontal => tuple.0,
Axis::Vertical => tuple.1,
}
}
impl PointerAxisEvent<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
fn amount(&self, axis: Axis) -> Option<f64> {
Some(tuple_axis(self.frame.axis, axis))
}
fn amount_v120(&self, axis: Axis) -> Option<f64> {
self.frame.v120.map(|v120| tuple_axis(v120, axis) as f64)
}
fn source(&self) -> AxisSource {
self.frame.source.unwrap_or_else(|| {
warn!("AxisSource: no source set, giving bogus value");
AxisSource::Continuous
})
}
fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection {
tuple_axis(self.frame.relative_direction, axis)
}
}
impl PointerMotionAbsoluteEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {}
impl InputBackend for VirtualPointerInputBackend {
type Device = VirtualPointer;
type KeyboardKeyEvent = UnusedEvent;
type PointerAxisEvent = VirtualPointerAxisEvent;
type PointerButtonEvent = VirtualPointerButtonEvent;
type PointerMotionEvent = VirtualPointerMotionEvent;
type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
pub trait VirtualPointerHandler {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState;
fn create_virtual_pointer(&mut self, pointer: VirtualPointer) {
let _ = pointer;
}
fn destroy_virtual_pointer(&mut self, pointer: VirtualPointer) {
let _ = pointer;
}
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent);
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent);
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent);
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent);
}
impl VirtualPointerManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = VirtualPointerManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrVirtualPointerManagerV1, _>(VERSION, global_data);
Self {
virtual_pointers: HashSet::new(),
}
}
}
impl<D> GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData, D>
for VirtualPointerManagerState
where
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<ZwlrVirtualPointerManagerV1>,
_manager_state: &VirtualPointerManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &VirtualPointerManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrVirtualPointerManagerV1, (), D> for VirtualPointerManagerState
where
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
_resource: &ZwlrVirtualPointerManagerV1,
request: <ZwlrVirtualPointerManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let (id, seat, output) = match request {
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => {
(id, seat, None)
}
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput {
seat,
output,
id,
} => (id, seat, output.as_ref().and_then(Output::from_resource)),
zwlr_virtual_pointer_manager_v1::Request::Destroy => return,
_ => unreachable!(),
};
let pointer = data_init.init(
id,
VirtualPointerUserData {
seat,
output,
axis_frame: Mutex::new(None),
},
);
state
.virtual_pointer_manager_state()
.virtual_pointers
.insert(pointer.clone());
state.create_virtual_pointer(VirtualPointer { pointer });
}
}
impl<D> Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData, D> for VirtualPointerManagerState
where
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
D: VirtualPointerHandler,
D: 'static,
{
fn request(
handler: &mut D,
_client: &Client,
resource: &ZwlrVirtualPointerV1,
request: <ZwlrVirtualPointerV1 as Resource>::Request,
_data: &VirtualPointerUserData,
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let pointer = VirtualPointer {
pointer: resource.clone(),
};
match request {
zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => {
let event = VirtualPointerMotionEvent {
pointer,
time,
dx,
dy,
};
handler.on_virtual_pointer_motion(event);
}
zwlr_virtual_pointer_v1::Request::MotionAbsolute {
time,
x,
y,
x_extent,
y_extent,
} => {
let event = VirtualPointerMotionAbsoluteEvent {
pointer,
time,
x,
y,
x_extent,
y_extent,
};
handler.on_virtual_pointer_motion_absolute(event);
}
zwlr_virtual_pointer_v1::Request::Button {
time,
button,
state,
} => {
// state is an enum but wlroots treats it as a C boolean (zero or nonzero)
// so we emulate that behaviour too. ButtonState::Pressed and any invalid value
// counts as pressed.
// https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/3187479c07c34a4de82c06a316a763a36a0499da/types/wlr_virtual_pointer_v1.c#L74
let state = match state {
WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released,
_ => ButtonState::Pressed,
};
let event = VirtualPointerButtonEvent {
pointer,
time,
button,
state,
};
handler.on_virtual_pointer_button(event);
}
zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("Axis: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| frame.value(axis, value));
}
zwlr_virtual_pointer_v1::Request::Frame => {
if let Some(frame) = pointer.finish_axis_frame() {
let event = VirtualPointerAxisEvent { pointer, frame };
handler.on_virtual_pointer_axis(event);
}
}
zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => {
let axis_source = match axis_source {
WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel,
WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger,
WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous,
WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt,
_ => {
warn!("AxisSource: invalid axis source");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxisSource,
"invalid axis source",
);
return;
}
};
pointer.mutate_axis_frame(None, |frame| frame.source(axis_source));
}
zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("AxisStop: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| frame.stop(axis));
}
zwlr_virtual_pointer_v1::Request::AxisDiscrete {
time,
axis,
value,
discrete,
} => {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
_ => {
warn!("AxisDiscrete: invalid axis");
resource.post_error(
zwlr_virtual_pointer_v1::Error::InvalidAxis,
"invalid axis",
);
return;
}
};
pointer.mutate_axis_frame(Some(time), |frame| {
frame.value(axis, value).v120(axis, discrete)
});
}
zwlr_virtual_pointer_v1::Request::Destroy => {}
_ => unreachable!(),
}
}
fn destroyed(
handler: &mut D,
_client: wayland_backend::server::ClientId,
resource: &ZwlrVirtualPointerV1,
_data: &VirtualPointerUserData,
) {
let pointer = VirtualPointer {
pointer: resource.clone(),
};
handler.destroy_virtual_pointer(pointer);
handler
.virtual_pointer_manager_state()
.virtual_pointers
.remove(resource);
}
}
#[macro_export]
macro_rules! delegate_virtual_pointer {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: ()
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerUserData
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
};
}
+76 -14
View File
@@ -36,7 +36,7 @@ use smithay::backend::drm::DrmDeviceFd;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{Output, OutputModeSource, WeakOutput};
use smithay::output::{Output, OutputModeSource};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
@@ -44,8 +44,8 @@ use smithay::utils::{Physical, Scale, Size, Transform};
use zbus::object_server::SignalEmitter;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State;
use crate::render_helpers::render_to_dmabuf;
use crate::niri::{CastTarget, State};
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
use crate::utils::get_monotonic_time;
// Give a 0.1 ms allowance for presentation time errors.
@@ -60,16 +60,18 @@ pub struct PipeWire {
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw(CastTarget),
Redraw { stream_id: usize },
FatalError,
}
pub struct Cast {
pub session_id: usize,
pub stream_id: usize,
pub stream: Stream,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub target: CastTarget,
pub dynamic_target: bool,
formats: FormatSet,
state: Rc<RefCell<CastState>>,
refresh: Rc<Cell<u32>>,
@@ -108,12 +110,6 @@ pub enum CastSizeChange {
Pending,
}
#[derive(Clone, PartialEq, Eq)]
pub enum CastTarget {
Output(WeakOutput),
Window { id: u64 },
}
macro_rules! make_params {
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
let mut b1 = Vec::new();
@@ -189,7 +185,9 @@ impl PipeWire {
gbm: GbmDevice<DrmDeviceFd>,
formats: FormatSet,
session_id: usize,
stream_id: usize,
target: CastTarget,
dynamic_target: bool,
size: Size<i32, Physical>,
refresh: u32,
alpha: bool,
@@ -204,10 +202,9 @@ impl PipeWire {
warn!("error sending StopCast to niri: {err:?}");
}
};
let target_ = target.clone();
let to_niri_ = self.to_niri.clone();
let redraw = move || {
if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
if let Err(err) = to_niri_.send(PwToNiri::Redraw { stream_id }) {
warn!("error sending Redraw to niri: {err:?}");
}
};
@@ -651,10 +648,12 @@ impl PipeWire {
let cast = Cast {
session_id,
stream_id,
stream,
_listener: listener,
is_active,
target,
dynamic_target,
formats,
state,
refresh,
@@ -822,6 +821,7 @@ impl Cast {
elements: &[impl RenderElement<GlesRenderer>],
size: Size<i32, Physical>,
scale: Scale<f64>,
wait_for_sync: bool,
) -> bool {
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
error!("cast must be in Ready state to render");
@@ -852,7 +852,7 @@ impl Cast {
let fd = buffer.datas_mut()[0].as_raw().fd;
let dmabuf = &self.dmabufs.borrow()[&fd];
if let Err(err) = render_to_dmabuf(
match render_to_dmabuf(
renderer,
dmabuf.clone(),
size,
@@ -860,8 +860,70 @@ impl Cast {
Transform::Normal,
elements.iter().rev(),
) {
warn!("error rendering to dmabuf: {err:?}");
Ok(sync_point) => {
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
if wait_for_sync {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = sync_point.wait() {
warn!("error waiting for pw frame completion: {err:?}");
}
}
}
Err(err) => {
warn!("error rendering to dmabuf: {err:?}");
return false;
}
}
for (data, (stride, offset)) in
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
{
let chunk = data.chunk_mut();
*chunk.size_mut() = 1;
*chunk.stride_mut() = stride as i32;
*chunk.offset_mut() = offset;
trace!(
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
data.as_raw().fd
);
}
true
}
pub fn dequeue_buffer_and_clear(
&mut self,
renderer: &mut GlesRenderer,
wait_for_sync: bool,
) -> bool {
// Clear out the damage tracker if we're in Ready state.
if let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() {
*damage_tracker = None;
};
let Some(mut buffer) = self.stream.dequeue_buffer() else {
warn!("no available buffer in pw stream, skipping clear");
return false;
};
let fd = buffer.datas_mut()[0].as_raw().fd;
let dmabuf = &self.dmabufs.borrow()[&fd];
match clear_dmabuf(renderer, dmabuf.clone()) {
Ok(sync_point) => {
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
if wait_for_sync {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = sync_point.wait() {
warn!("error waiting for pw frame completion: {err:?}");
}
}
}
Err(err) => {
warn!("error clearing dmabuf: {err:?}");
return false;
}
}
for (data, (stride, offset)) in
+10 -2
View File
@@ -39,6 +39,7 @@ struct Parameters {
corner_radius: CornerRadius,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
alpha: f32,
}
impl BorderRenderElement {
@@ -54,6 +55,7 @@ impl BorderRenderElement {
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
alpha: f32,
) -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
let mut rv = Self {
@@ -69,6 +71,7 @@ impl BorderRenderElement {
border_width,
corner_radius,
scale,
alpha,
},
};
rv.update_inner();
@@ -90,6 +93,7 @@ impl BorderRenderElement {
border_width: 0.,
corner_radius: Default::default(),
scale: 1.,
alpha: 1.,
},
}
}
@@ -111,6 +115,7 @@ impl BorderRenderElement {
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
alpha: f32,
) {
let params = Parameters {
size,
@@ -123,6 +128,7 @@ impl BorderRenderElement {
border_width,
corner_radius,
scale,
alpha,
};
if self.params == params {
return;
@@ -144,6 +150,7 @@ impl BorderRenderElement {
border_width,
corner_radius,
scale,
alpha,
} = self.params;
let grad_offset = geometry.loc - gradient_area.loc;
@@ -189,6 +196,7 @@ impl BorderRenderElement {
size,
None,
scale,
alpha,
vec![
Uniform::new("colorspace", colorspace),
Uniform::new("hue_interpolation", hue_interpolation),
@@ -269,7 +277,7 @@ impl Element for BorderRenderElement {
impl RenderElement<GlesRenderer> for BorderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
@@ -286,7 +294,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
+15 -30
View File
@@ -19,9 +19,7 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
program: GlesTexProgram,
corner_radius: CornerRadius,
geometry: Rectangle<f64, Logical>,
input_to_geo: Mat3,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
uniforms: Vec<Uniform<'static>>,
}
#[derive(Debug, Default, Clone)]
@@ -72,13 +70,19 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
* Mat3::from_scale(buf_size / src_size)
* Mat3::from_translation(-src_loc / buf_size);
let uniforms = vec![
Uniform::new("niri_scale", scale.x as f32),
Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)),
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
mat3_uniform("input_to_geo", input_to_geo),
];
Self {
inner: elem,
program,
corner_radius,
geometry,
input_to_geo,
scale: scale.x as f32,
uniforms,
}
}
@@ -214,24 +218,13 @@ impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
frame.override_default_tex_program(
self.program.clone(),
vec![
Uniform::new("niri_scale", self.scale),
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
frame.override_default_tex_program(self.program.clone(), self.uniforms.clone());
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.clear_tex_program_override();
Ok(())
@@ -249,23 +242,15 @@ impl<'render> RenderElement<TtyRenderer<'render>>
{
fn draw(
&self,
frame: &mut TtyFrame<'render, '_>,
frame: &mut TtyFrame<'render, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
frame.as_gles_frame().override_default_tex_program(
self.program.clone(),
vec![
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
frame
.as_gles_frame()
.override_default_tex_program(self.program.clone(), self.uniforms.clone());
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.as_gles_frame().clear_tex_program_override();
Ok(())
+1 -1
View File
@@ -65,7 +65,7 @@ impl Element for ExtraDamage {
impl<R: Renderer> RenderElement<R> for ExtraDamage {
fn draw(
&self,
_frame: &mut <R as Renderer>::Frame<'_>,
_frame: &mut R::Frame<'_, '_>,
_src: Rectangle<f64, Buffer>,
_dst: Rectangle<i32, Physical>,
_damage: &[Rectangle<i32, Physical>],
+49 -18
View File
@@ -6,7 +6,7 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::{Buffer, Fourcc};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTarget, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
@@ -31,6 +31,7 @@ pub mod resize;
pub mod resources;
pub mod shader_element;
pub mod shaders;
pub mod shadow;
pub mod snapshot;
pub mod solid_color;
pub mod surface;
@@ -156,6 +157,16 @@ impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
}
}
pub fn encompassing_geo(
scale: Scale<f64>,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> Rectangle<i32, Physical> {
elements
.map(|ele| ele.geometry(scale))
.reduce(|a, b| a.merge(b))
.unwrap_or_default()
}
pub fn render_to_encompassing_texture(
renderer: &mut GlesRenderer,
scale: Scale<f64>,
@@ -163,13 +174,9 @@ pub fn render_to_encompassing_texture(
fourcc: Fourcc,
elements: &[impl RenderElement<GlesRenderer>],
) -> anyhow::Result<(GlesTexture, SyncPoint, Rectangle<i32, Physical>)> {
let geo = elements
.iter()
.map(|ele| ele.geometry(scale))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let geo = encompassing_geo(scale, elements.iter());
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
});
let (texture, sync_point) =
@@ -190,15 +197,18 @@ pub fn render_to_texture(
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let texture: GlesTexture = renderer
let mut texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
renderer
.bind(texture.clone())
.context("error binding texture")?;
let sync_point = {
let mut target = renderer
.bind(&mut texture)
.context("error binding texture")?;
render_elements(renderer, &mut target, size, scale, transform, elements)?
};
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
Ok((texture, sync_point))
}
@@ -212,11 +222,16 @@ pub fn render_and_download(
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!();
let (_, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
let (mut texture, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
// FIXME: would be nice to avoid binding the second time here (after render_to_texture()), but
// borrowing makes this invonvenient.
let target = renderer
.bind(&mut texture)
.context("error binding texture")?;
let mapping = renderer
.copy_framebuffer(Rectangle::from_size(buffer_size), fourcc)
.copy_framebuffer(&target, Rectangle::from_size(buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
@@ -241,7 +256,7 @@ pub fn render_to_vec(
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: Dmabuf,
mut dmabuf: Dmabuf,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
@@ -252,8 +267,10 @@ pub fn render_to_dmabuf(
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
"invalid buffer size"
);
renderer.bind(dmabuf).context("error binding texture")?;
render_elements(renderer, size, scale, transform, elements)
let mut target = renderer
.bind(&mut dmabuf)
.context("error binding texture")?;
render_elements(renderer, &mut target, size, scale, transform, elements)
}
pub fn render_to_shm(
@@ -292,8 +309,22 @@ pub fn render_to_shm(
.context("expected shm buffer, but didn't get one")?
}
pub fn clear_dmabuf(renderer: &mut GlesRenderer, mut dmabuf: Dmabuf) -> anyhow::Result<SyncPoint> {
let size = dmabuf.size();
let size = size.to_logical(1, Transform::Normal).to_physical(1);
let mut target = renderer.bind(&mut dmabuf).context("error binding dmabuf")?;
let mut frame = renderer
.render(&mut target, size, Transform::Normal)
.context("error starting frame")?;
frame
.clear(Color32F::TRANSPARENT, &[Rectangle::from_size(size)])
.context("error clearing")?;
frame.finish().context("error finishing frame")
}
fn render_elements(
renderer: &mut GlesRenderer,
target: &mut GlesTarget,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
@@ -303,7 +334,7 @@ fn render_elements(
let output_rect = Rectangle::from_size(transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
.render(target, size, transform)
.context("error starting frame")?;
frame
+281 -177
View File
@@ -1,134 +1,264 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use std::cell::RefCell;
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use super::texture::{TextureBuffer, TextureRenderElement};
use anyhow::Context as _;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{
Element, Id, Kind, RenderElement, RenderElementStates, UnderlyingStorage,
};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::utils::{
CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions,
};
use smithay::backend::renderer::{
Bind as _, Color32F, Frame as _, Offscreen as _, Renderer, Texture as _,
};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::encompassing_geo;
use super::renderer::AsGlesFrame as _;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders elements into an off-screen buffer.
/// Buffer for offscreen rendering.
#[derive(Debug)]
pub struct OffscreenBuffer {
id: Id,
/// The cached texture buffer.
///
/// Lazily created when `render` is called. Recreated when necessary.
inner: RefCell<Option<Inner>>,
}
#[derive(Debug)]
struct Inner {
/// The texture with offscreened contents.
texture: GlesTexture,
/// Id of the renderer that the texture comes from.
renderer_id: usize,
/// Scale of the texture.
scale: Scale<f64>,
/// Damage tracker for drawing to the texture.
damage: OutputDamageTracker,
/// Damage of this offscreen element itself facing outside.
outer_damage: DamageBag<i32, Buffer>,
}
#[derive(Debug, Clone)]
pub struct OffscreenRenderElement {
// The texture, if rendering succeeded.
texture: Option<PrimaryGpuTextureRenderElement>,
// The fallback buffer in case the rendering fails.
fallback: SolidColorRenderElement,
id: Id,
texture: GlesTexture,
renderer_id: usize,
scale: Scale<f64>,
damage: DamageSnapshot<i32, Buffer>,
offset: Point<f64, Logical>,
src_size: Size<i32, Buffer>,
alpha: f32,
kind: Kind,
}
#[derive(Debug)]
pub struct OffscreenData {
/// Id of the offscreen element.
pub id: Id,
/// States for the render into the offscreen buffer.
pub states: RenderElementStates,
}
impl OffscreenBuffer {
pub fn render(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
elements: &[impl RenderElement<GlesRenderer>],
) -> anyhow::Result<(OffscreenRenderElement, SyncPoint, OffscreenData)> {
let _span = tracy_client::span!("OffscreenBuffer::render");
let geo = encompassing_geo(scale, elements.iter());
let elements = Vec::from_iter(elements.iter().map(|ele| {
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
}));
let src_size = geo.size;
let src_size = src_size.to_logical(1).to_buffer(1, Transform::Normal);
let offset = geo.loc.to_f64().to_logical(scale);
let mut inner = self.inner.borrow_mut();
// Check if we need to create or recreate the texture.
let size_string;
let mut reason = "";
if let Some(Inner {
texture,
renderer_id,
..
}) = inner.as_mut()
{
let old_size = texture.size();
if old_size.w < src_size.w || old_size.h < src_size.h {
size_string = format!(
"size increased from {} × {} to {} × {}",
old_size.w, old_size.h, src_size.w, src_size.h
);
reason = &size_string;
*inner = None;
} else if !texture.is_unique_reference() {
reason = "not unique";
*inner = None;
} else if *renderer_id != renderer.id() {
reason = "renderer id changed";
*inner = None;
}
} else {
reason = "first render";
}
let inner = if let Some(inner) = inner.as_mut() {
inner
} else {
trace!("creating new texture: {reason}");
let span = tracy_client::span!("creating offscreen buffer");
span.emit_text(reason);
let texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, src_size)
.context("error creating texture")?;
let buffer_size = src_size.to_logical(1, Transform::Normal).to_physical(1);
let damage = OutputDamageTracker::new(buffer_size, scale, Transform::Normal);
inner.insert(Inner {
texture,
renderer_id: renderer.id(),
scale,
damage,
outer_damage: DamageBag::default(),
})
};
// When leaving the old texture as is, its size might be bigger than src_size.
let texture_size = inner.texture.size();
let buffer_size = texture_size.to_logical(1, Transform::Normal).to_physical(1);
// Recreate the damage tracker if the scale changes. We already recreate it for buffer size
// changes, and transform is always Normal.
if inner.scale != scale {
inner.scale = scale;
trace!("recreating damage tracker due to scale change");
inner.damage = OutputDamageTracker::new(buffer_size, scale, Transform::Normal);
inner.outer_damage = DamageBag::default();
}
let res = {
let mut target = renderer.bind(&mut inner.texture)?;
inner.damage.render_output(
renderer,
&mut target,
1,
&elements,
Color32F::TRANSPARENT,
)?
};
// Add the resulting damage to the outer tracker.
if let Some(damage) = res.damage {
// OutputDamageTracker gives us Physical coordinate space, but it's actually the Buffer
// space because we were rendering to a texture.
let size = buffer_size.to_logical(1);
let damage = damage
.iter()
.map(|rect| rect.to_logical(1).to_buffer(1, Transform::Normal, &size));
inner.outer_damage.add(damage);
}
let elem = OffscreenRenderElement {
id: self.id.clone(),
texture: inner.texture.clone(),
renderer_id: inner.renderer_id,
scale,
damage: inner.outer_damage.snapshot(),
offset,
src_size,
alpha: 1.,
kind: Kind::Unspecified,
};
let data = OffscreenData {
id: self.id.clone(),
states: res.states,
};
Ok((elem, res.sync, data))
}
}
impl Default for OffscreenBuffer {
fn default() -> Self {
OffscreenBuffer {
inner: RefCell::new(None),
id: Id::new(),
}
}
}
impl OffscreenRenderElement {
pub fn new(
renderer: &mut GlesRenderer,
scale: i32,
elements: &[impl RenderElement<GlesRenderer>],
result_alpha: f32,
) -> Self {
let _span = tracy_client::span!("OffscreenRenderElement::new");
pub fn texture(&self) -> &GlesTexture {
&self.texture
}
let geo = elements
.iter()
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let logical_size = geo.size.to_logical(scale);
pub fn offset(&self) -> Point<f64, Logical> {
self.offset
}
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
let fallback = SolidColorRenderElement::from_buffer(
&fallback_buffer,
geo.loc,
Scale::from(scale as f64),
result_alpha,
Kind::Unspecified,
);
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
});
pub fn with_offset(mut self, offset: Point<f64, Logical>) -> Self {
self.offset = offset;
self
}
match render_to_texture(
renderer,
geo.size,
Scale::from(scale as f64),
Transform::Normal,
Fourcc::Abgr8888,
elements,
) {
Ok((texture, _sync_point)) => {
let buffer = TextureBuffer::from_texture(
renderer,
texture,
scale as f64,
Transform::Normal,
Vec::new(),
);
let element = TextureRenderElement::from_texture_buffer(
buffer,
geo.loc.to_f64().to_logical(scale as f64),
result_alpha,
None,
None,
Kind::Unspecified,
);
Self {
texture: Some(PrimaryGpuTextureRenderElement(element)),
fallback,
}
}
Err(err) => {
warn!("error off-screening elements: {err:?}");
Self {
texture: None,
fallback,
}
}
}
pub fn logical_size(&self) -> Size<f64, Logical> {
self.src_size
.to_f64()
.to_logical(self.scale, Transform::Normal)
}
fn damage_since(&self, commit: Option<CommitCounter>) -> DamageSet<i32, Buffer> {
self.damage
.damage_since(commit)
.unwrap_or_else(|| DamageSet::from_slice(&[Rectangle::from_size(self.texture.size())]))
}
}
impl Element for OffscreenRenderElement {
fn id(&self) -> &Id {
if let Some(texture) = &self.texture {
texture.id()
} else {
self.fallback.id()
}
&self.id
}
fn current_commit(&self) -> CommitCounter {
if let Some(texture) = &self.texture {
texture.current_commit()
} else {
self.fallback.current_commit()
}
self.damage.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
if let Some(texture) = &self.texture {
texture.geometry(scale)
} else {
self.fallback.geometry(scale)
}
let logical_geo = Rectangle::new(self.offset, self.logical_size());
logical_geo.to_physical_precise_round(scale)
}
fn transform(&self) -> Transform {
if let Some(texture) = &self.texture {
texture.transform()
} else {
self.fallback.transform()
}
Transform::Normal
}
fn src(&self) -> Rectangle<f64, Buffer> {
if let Some(texture) = &self.texture {
texture.src()
} else {
self.fallback.src()
}
Rectangle::from_size(self.src_size).to_f64()
}
fn damage_since(
@@ -136,116 +266,90 @@ impl Element for OffscreenRenderElement {
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if let Some(texture) = &self.texture {
texture.damage_since(scale, commit)
} else {
self.fallback.damage_since(scale, commit)
}
let texture_size = self.texture.size().to_f64();
let src = self.src();
self.damage_since(commit)
.into_iter()
.filter_map(|region| {
let mut region = region.to_f64().intersection(src)?;
region.loc -= src.loc;
region.upscale(texture_size / src.size);
let logical = region.to_logical(self.scale, Transform::Normal, &src.size);
Some(logical.to_physical_precise_up(scale))
})
.collect()
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if let Some(texture) = &self.texture {
texture.opaque_regions(scale)
} else {
self.fallback.opaque_regions(scale)
}
fn opaque_regions(&self, _scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
OpaqueRegions::default()
}
fn alpha(&self) -> f32 {
if let Some(texture) = &self.texture {
texture.alpha()
} else {
self.fallback.alpha()
}
self.alpha
}
fn kind(&self) -> Kind {
if let Some(texture) = &self.texture {
texture.kind()
} else {
self.fallback.kind()
}
self.kind
}
}
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(
texture,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
if frame.id() != self.renderer_id {
warn!("trying to render texture from different renderer");
return Ok(());
}
Ok(())
frame.render_texture_from_to(
&self.texture,
src,
dest,
damage,
opaque_regions,
Transform::Normal,
self.alpha,
None,
&[],
)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(
texture,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
}
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+2 -2
View File
@@ -56,7 +56,7 @@ impl Element for PrimaryGpuTextureRenderElement {
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
@@ -77,7 +77,7 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
+2 -2
View File
@@ -99,7 +99,7 @@ macro_rules! niri_render_elements {
{
fn draw(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
@@ -124,7 +124,7 @@ macro_rules! niri_render_elements {
{
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
+10 -9
View File
@@ -1,7 +1,7 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, RendererSuper, Texture,
};
use crate::backend::tty::{TtyFrame, TtyRenderer};
@@ -21,7 +21,7 @@ pub trait NiriRenderer:
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ From<<GlesRenderer as RendererSuper>::Error>
+ 'static;
}
@@ -29,7 +29,8 @@ impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + Send + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
R::Error:
std::error::Error + Send + Sync + From<<GlesRenderer as RendererSuper>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
@@ -53,21 +54,21 @@ impl AsGlesRenderer for TtyRenderer<'_> {
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
pub trait AsGlesFrame<'frame, 'buffer>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
impl<'frame, 'buffer> AsGlesFrame<'frame, 'buffer> for GlesFrame<'frame, 'buffer> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer> {
self
}
}
impl<'frame> AsGlesFrame<'frame> for TtyFrame<'_, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
impl<'frame, 'buffer> AsGlesFrame<'frame, 'buffer> for TtyFrame<'_, 'frame, 'buffer> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer> {
self.as_mut()
}
}
+9 -8
View File
@@ -5,6 +5,7 @@ use niri_config::CornerRadius;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::backend::renderer::Texture as _;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::renderer::{AsGlesFrame, NiriRenderer};
@@ -56,10 +57,10 @@ impl ResizeRenderElement {
let curr_geo_size = Vec2::new(curr_geo.size.w as f32, curr_geo.size.h as f32);
let tex_prev_geo_loc = Vec2::new(tex_prev_geo.loc.x as f32, tex_prev_geo.loc.y as f32);
let tex_prev_geo_size = Vec2::new(tex_prev_geo.size.w as f32, tex_prev_geo.size.h as f32);
let tex_prev_size = Vec2::new(texture_prev.width() as f32, texture_prev.height() as f32);
let tex_next_geo_loc = Vec2::new(tex_next_geo.loc.x as f32, tex_next_geo.loc.y as f32);
let tex_next_geo_size = Vec2::new(tex_next_geo.size.w as f32, tex_next_geo.size.h as f32);
let tex_next_size = Vec2::new(texture_next.width() as f32, texture_next.height() as f32);
let size_prev = Vec2::new(size_prev.w as f32, size_prev.h as f32);
let size_next = Vec2::new(size_next.w as f32, size_next.h as f32);
@@ -73,10 +74,10 @@ impl ResizeRenderElement {
let curr_geo_to_prev_geo = Mat3::from_scale(curr_geo_size / size_prev);
let curr_geo_to_next_geo = Mat3::from_scale(curr_geo_size / size_next);
let geo_to_tex_prev = Mat3::from_translation(-tex_prev_geo_loc / tex_prev_geo_size)
* Mat3::from_scale(size_prev / tex_prev_geo_size * scale);
let geo_to_tex_next = Mat3::from_translation(-tex_next_geo_loc / tex_next_geo_size)
* Mat3::from_scale(size_next / tex_next_geo_size * scale);
let geo_to_tex_prev = Mat3::from_translation(-tex_prev_geo_loc / tex_prev_size)
* Mat3::from_scale(size_prev / tex_prev_size * scale);
let geo_to_tex_next = Mat3::from_translation(-tex_next_geo_loc / tex_next_size)
* Mat3::from_scale(size_next / tex_next_size * scale);
let corner_radius = corner_radius.fit_to(curr_geo_size.x, curr_geo_size.y);
let clip_to_geometry = if clip_to_geometry { 1. } else { 0. };
@@ -163,7 +164,7 @@ impl Element for ResizeRenderElement {
impl RenderElement<GlesRenderer> for ResizeRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
@@ -181,7 +182,7 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
+12 -5
View File
@@ -227,12 +227,14 @@ impl ShaderRenderElement {
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
scale: f32,
alpha: f32,
uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
) {
self.area.size = size;
self.opaque_regions = opaque_regions.unwrap_or_default();
self.scale = scale;
self.alpha = alpha;
self.additional_uniforms = uniforms;
self.textures = textures;
@@ -243,6 +245,11 @@ impl ShaderRenderElement {
self.area.loc = location;
self
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
}
impl Element for ShaderRenderElement {
@@ -270,7 +277,7 @@ impl Element for ShaderRenderElement {
}
fn alpha(&self) -> f32 {
1.0
self.alpha
}
fn kind(&self) -> Kind {
@@ -281,7 +288,7 @@ impl Element for ShaderRenderElement {
impl RenderElement<GlesRenderer> for ShaderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
@@ -308,7 +315,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
let rect_constrained_loc = rect
.loc
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
.constrain(Rectangle::from_extremities((0, 0), dest_size.to_point()));
let rect_clamped_size = rect.size.clamp(
(0, 0),
(dest_size.to_point() - rect_constrained_loc).to_size(),
@@ -328,7 +335,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
let rect_constrained_loc = rect
.loc
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
.constrain(Rectangle::from_extremities((0, 0), dest_size.to_point()));
let rect_clamped_size = rect.size.clamp(
(0, 0),
(dest_size.to_point() - rect_constrained_loc).to_size(),
@@ -512,7 +519,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
impl<'render> RenderElement<TtyRenderer<'render>> for ShaderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
+25 -1
View File
@@ -11,6 +11,7 @@ use super::shader_element::ShaderProgram;
pub struct Shaders {
pub border: Option<ShaderProgram>,
pub shadow: Option<ShaderProgram>,
pub clipped_surface: Option<GlesTexProgram>,
pub resize: Option<ShaderProgram>,
pub custom_resize: RefCell<Option<ShaderProgram>>,
@@ -21,6 +22,7 @@ pub struct Shaders {
#[derive(Debug, Clone, Copy)]
pub enum ProgramType {
Border,
Shadow,
Resize,
Close,
Open,
@@ -53,6 +55,26 @@ impl Shaders {
})
.ok();
let shadow = ShaderProgram::compile(
renderer,
include_str!("shadow.frag"),
&[
UniformName::new("shadow_color", UniformType::_4f),
UniformName::new("sigma", UniformType::_1f),
UniformName::new("input_to_geo", UniformType::Matrix3x3),
UniformName::new("geo_size", UniformType::_2f),
UniformName::new("corner_radius", UniformType::_4f),
UniformName::new("window_input_to_geo", UniformType::Matrix3x3),
UniformName::new("window_geo_size", UniformType::_2f),
UniformName::new("window_corner_radius", UniformType::_4f),
],
&[],
)
.map_err(|err| {
warn!("error compiling shadow shader: {err:?}");
})
.ok();
let clipped_surface = renderer
.compile_custom_texture_shader(
include_str!("clipped_surface.frag"),
@@ -76,6 +98,7 @@ impl Shaders {
Self {
border,
shadow,
clipped_surface,
resize,
custom_resize: RefCell::new(None),
@@ -84,7 +107,7 @@ impl Shaders {
}
}
pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_>) -> &'a Self {
pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_, '_>) -> &'a Self {
let data = frame.egl_context().user_data();
data.get()
.expect("shaders::init() must be called when creating the renderer")
@@ -121,6 +144,7 @@ impl Shaders {
pub fn program(&self, program: ProgramType) -> Option<ShaderProgram> {
match program {
ProgramType::Border => self.border.clone(),
ProgramType::Shadow => self.shadow.clone(),
ProgramType::Resize => self
.custom_resize
.borrow()
+142
View File
@@ -0,0 +1,142 @@
precision highp float;
#if defined(DEBUG_FLAGS)
uniform float niri_tint;
#endif
uniform float niri_alpha;
uniform float niri_scale;
uniform vec2 niri_size;
varying vec2 niri_v_coords;
uniform vec4 shadow_color;
uniform float sigma;
uniform mat3 input_to_geo;
uniform vec2 geo_size;
uniform vec4 corner_radius;
uniform mat3 window_input_to_geo;
uniform vec2 window_geo_size;
uniform vec4 window_corner_radius;
// Based on: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
//
// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/)
// A standard gaussian function, used for weighting samples
float gaussian(float x, float sigma) {
const float pi = 3.141592653589793;
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma);
}
// This approximates the error function, needed for the gaussian integral
vec2 erf(vec2 x) {
vec2 s = sign(x), a = abs(x);
x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
}
// Return the blurred mask along the x dimension
float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) {
float delta = min(halfSize.y - corner - abs(y), 0.0);
float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
return integral.y - integral.x;
}
// Return the mask for the shadow of a box from lower to upper
float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) {
// Center everything to make the math easier
vec2 center = (lower + upper) * 0.5;
vec2 halfSize = (upper - lower) * 0.5;
point -= center;
// The signal is only non-zero in a limited range, so don't waste samples
float low = point.y - halfSize.y;
float high = point.y + halfSize.y;
float start = clamp(-3.0 * sigma, low, high);
float end = clamp(3.0 * sigma, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.0;
float y = start + step * 0.5;
float value = 0.0;
for (int i = 0; i < 4; i++) {
value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step;
y += step;
}
return value;
}
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float dist = distance(coords, center);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0);
vec3 coords_window_geo = window_input_to_geo * vec3(niri_v_coords, 1.0);
vec4 color = shadow_color;
float shadow_value;
if (sigma < 0.1) {
// With low enough sigma just draw a rounded rectangle.
shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius);
} else {
shadow_value = roundedBoxShadow(
vec2(0.0, 0.0),
geo_size,
coords_geo.xy,
sigma,
// FIXME: figure out how to blur with different corner radii.
//
// GTK seems to call blurring separately for the rect and for the 4 corners:
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gsk/gpu/shaders/gskgpuboxshadow.glsl
corner_radius.x
);
}
color = color * shadow_value;
// Cut out the inside of the window geometry if requested.
if (window_geo_size != vec2(0.0, 0.0)) {
if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x
&& 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) {
float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius);
color = color * (1.0 - alpha);
}
}
color = color * niri_alpha;
#if defined(DEBUG_FLAGS)
if (niri_tint == 1.0)
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
#endif
gl_FragColor = color;
}
+270
View File
@@ -0,0 +1,270 @@
use std::collections::HashMap;
use glam::{Mat3, Vec2};
use niri_config::{Color, CornerRadius};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::renderer::NiriRenderer;
use super::shader_element::ShaderRenderElement;
use super::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a rounded rectangle shadow.
#[derive(Debug, Clone)]
pub struct ShadowRenderElement {
inner: ShaderRenderElement,
params: Parameters,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Parameters {
size: Size<f64, Logical>,
geometry: Rectangle<f64, Logical>,
color: Color,
sigma: f32,
corner_radius: CornerRadius,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
alpha: f32,
window_geometry: Rectangle<f64, Logical>,
window_corner_radius: CornerRadius,
}
impl ShadowRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
size: Size<f64, Logical>,
geometry: Rectangle<f64, Logical>,
color: Color,
sigma: f32,
corner_radius: CornerRadius,
scale: f32,
window_geometry: Rectangle<f64, Logical>,
window_corner_radius: CornerRadius,
alpha: f32,
) -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified);
let mut rv = Self {
inner,
params: Parameters {
size,
geometry,
color,
sigma,
corner_radius,
scale,
alpha,
window_geometry,
window_corner_radius,
},
};
rv.update_inner();
rv
}
pub fn empty() -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified);
Self {
inner,
params: Parameters {
size: Default::default(),
geometry: Default::default(),
color: Default::default(),
sigma: 0.,
corner_radius: Default::default(),
scale: 1.,
alpha: 1.,
window_geometry: Default::default(),
window_corner_radius: Default::default(),
},
}
}
pub fn damage_all(&mut self) {
self.inner.damage_all();
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
size: Size<f64, Logical>,
geometry: Rectangle<f64, Logical>,
color: Color,
sigma: f32,
corner_radius: CornerRadius,
scale: f32,
window_geometry: Rectangle<f64, Logical>,
window_corner_radius: CornerRadius,
alpha: f32,
) {
let params = Parameters {
size,
geometry,
color,
sigma,
alpha,
corner_radius,
scale,
window_geometry,
window_corner_radius,
};
if self.params == params {
return;
}
self.params = params;
self.update_inner();
}
fn update_inner(&mut self) {
let Parameters {
size,
geometry,
color,
sigma,
alpha,
corner_radius,
scale,
window_geometry,
window_corner_radius,
} = self.params;
let area_size = Vec2::new(size.w as f32, size.h as f32);
let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32);
let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32);
let input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
let window_geo_loc = Vec2::new(window_geometry.loc.x as f32, window_geometry.loc.y as f32);
let window_geo_size =
Vec2::new(window_geometry.size.w as f32, window_geometry.size.h as f32);
let window_input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-window_geo_loc / area_size);
self.inner.update(
size,
None,
scale,
alpha,
vec![
Uniform::new("shadow_color", color.to_array_premul()),
Uniform::new("sigma", sigma),
mat3_uniform("input_to_geo", input_to_geo),
Uniform::new("geo_size", geo_size.to_array()),
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
mat3_uniform("window_input_to_geo", window_input_to_geo),
Uniform::new("window_geo_size", window_geo_size.to_array()),
Uniform::new(
"window_corner_radius",
<[f32; 4]>::from(window_corner_radius),
),
],
HashMap::new(),
);
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.inner = self.inner.with_alpha(alpha);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Shadow)
.is_some()
}
}
impl Default for ShadowRenderElement {
fn default() -> Self {
Self::empty()
}
}
impl Element for ShadowRenderElement {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.inner.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.inner.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for ShadowRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for ShadowRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
+2 -2
View File
@@ -153,12 +153,12 @@ impl Element for SolidColorRenderElement {
impl<R: Renderer> RenderElement<R> for SolidColorRenderElement {
fn draw(
&self,
frame: &mut <R as Renderer>::Frame<'_>,
frame: &mut R::Frame<'_, '_>,
_src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), <R as Renderer>::Error> {
) -> Result<(), R::Error> {
frame.draw_solid(dst, damage, self.color)
}
+11 -4
View File
@@ -1,5 +1,6 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::GlesTexture;
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::{Frame as _, ImportMem, Renderer, Texture};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
@@ -58,7 +59,7 @@ impl<T> TextureBuffer<T> {
scale: impl Into<Scale<f64>>,
transform: Transform,
opaque_regions: Vec<Rectangle<i32, Buffer>>,
) -> Result<Self, <R as Renderer>::Error> {
) -> Result<Self, R::Error> {
let texture = renderer.import_memory(data, format, size.into(), flipped)?;
Ok(TextureBuffer::from_texture(
renderer,
@@ -72,7 +73,7 @@ impl<T> TextureBuffer<T> {
pub fn from_memory_buffer<R: Renderer<TextureId = T> + ImportMem>(
renderer: &mut R,
buffer: &MemoryBuffer,
) -> Result<Self, <R as Renderer>::Error> {
) -> Result<Self, R::Error> {
Self::from_memory(
renderer,
buffer.data(),
@@ -115,6 +116,12 @@ impl<T: Texture> TextureBuffer<T> {
}
}
impl TextureBuffer<GlesTexture> {
pub fn is_texture_reference_unique(&mut self) -> bool {
self.texture.is_unique_reference()
}
}
impl<T> TextureRenderElement<T> {
pub fn from_texture_buffer(
buffer: TextureBuffer<T>,
@@ -213,12 +220,12 @@ where
{
fn draw(
&self,
frame: &mut <R as Renderer>::Frame<'_>,
frame: &mut R::Frame<'_, '_>,
src: Rectangle<f64, Buffer>,
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), <R as Renderer>::Error> {
) -> Result<(), R::Error> {
if frame.id() != self.buffer.renderer_id {
warn!("trying to render texture from different renderer");
return Ok(());
+7 -17
View File
@@ -1,13 +1,11 @@
use std::cmp::min;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt;
use std::fmt::Write as _;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::{env, fmt};
use calloop::EventLoop;
use calloop_wayland_source::WaylandSource;
@@ -64,7 +62,7 @@ pub struct Window {
pub viewport: WpViewport,
pub pending_configure: Configure,
pub configures_received: Vec<(u32, Configure)>,
pub close_requsted: bool,
pub close_requested: bool,
pub configures_looked_at: usize,
}
@@ -105,21 +103,13 @@ impl fmt::Display for Configure {
}
}
fn connect(socket_name: &OsStr) -> Connection {
let mut socket_path = PathBuf::from(env::var_os("XDG_RUNTIME_DIR").unwrap());
socket_path.push(socket_name);
let stream = UnixStream::connect(socket_path).unwrap();
let backend = Backend::connect(stream).unwrap();
Connection::from_backend(backend)
}
impl Client {
pub fn new(socket_name: &OsStr) -> Self {
pub fn new(stream: UnixStream) -> Self {
let id = ClientId::next();
let event_loop = EventLoop::try_new().unwrap();
let connection = connect(socket_name);
let backend = Backend::connect(stream).unwrap();
let connection = Connection::from_backend(backend);
let queue = connection.new_event_queue();
let qh = queue.handle();
WaylandSource::new(connection.clone(), queue)
@@ -204,7 +194,7 @@ impl State {
viewport,
pending_configure: Configure::default(),
configures_received: Vec::new(),
close_requsted: false,
close_requested: false,
configures_looked_at: 0,
};
@@ -469,7 +459,7 @@ impl Dispatch<XdgToplevel, ()> for State {
.collect();
}
xdg_toplevel::Event::Close => {
window.close_requsted = true;
window.close_requested = true;
}
xdg_toplevel::Event::ConfigureBounds { width, height } => {
window.pending_configure.bounds = Some((width, height));
+11 -3
View File
@@ -1,4 +1,5 @@
use std::os::fd::AsFd as _;
use std::os::unix::net::UnixStream;
use std::sync::atomic::Ordering;
use std::time::Duration;
@@ -9,7 +10,7 @@ use smithay::output::Output;
use super::client::{Client, ClientId};
use super::server::Server;
use crate::niri::Niri;
use crate::niri::{NewClient, Niri};
pub struct Fixture {
pub event_loop: EventLoop<'static, State>,
@@ -88,7 +89,14 @@ impl Fixture {
}
pub fn add_client(&mut self) -> ClientId {
let client = Client::new(&self.state.server.state.niri.socket_name);
let (sock1, sock2) = UnixStream::pair().unwrap();
self.niri().insert_client(NewClient {
client: sock1,
restricted: false,
credentials_unknown: false,
});
let client = Client::new(sock2);
let id = client.id;
let fd = client.event_loop.as_fd().try_clone_to_owned().unwrap();
@@ -117,7 +125,7 @@ impl Fixture {
}
}
/// Rountrip twice in a row.
/// Roundtrip twice in a row.
///
/// For some reason, when running tests on many threads at once, a single roundtrip is
/// sometimes not sufficient to get the configure events to the client.
+27
View File
@@ -854,3 +854,30 @@ window-rule {
@"size: 300 × 100, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn unfullscreen_to_floating_doesnt_send_extra_configure() {
let (mut f, id, surface) = set_up();
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.roundtrip(id);
// Fullscreen.
let window = f.client(id).window(&surface);
window.set_fullscreen(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Unfullscreen via the window request which requires a configure response.
let window = f.client(id).window(&surface);
window.unset_fullscreen();
f.double_roundtrip(id);
// This should configure only once and not twice.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 936 × 1048, bounds: 1920 × 1080, states: [Activated]"
);
}
+155
View File
@@ -0,0 +1,155 @@
use client::ClientId;
use insta::assert_snapshot;
use wayland_client::protocol::wl_surface::WlSurface;
use super::*;
use crate::layout::LayoutElement as _;
// Sets up a fixture with two outputs and 100×100 window.
fn set_up() -> (Fixture, ClientId, WlSurface) {
let mut f = Fixture::new();
f.add_output(1, (1920, 1080));
f.add_output(2, (1280, 720));
let id = f.add_client();
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.commit();
f.roundtrip(id);
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(100, 100);
window.ack_last_and_commit();
f.double_roundtrip(id);
(f, id, surface)
}
#[test]
fn windowed_fullscreen() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
// Enable windowed fullscreen.
niri.layout.toggle_windowed_fullscreen(&window_id);
f.double_roundtrip(id);
// Should request fullscreen state with the tiled size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]"
);
let mapped = f.niri().layout.windows().next().unwrap().1;
// Not committed yet.
assert!(!mapped.is_windowed_fullscreen());
// Commit in response.
let window = f.client(id).window(&surface);
window.ack_last_and_commit();
f.roundtrip(id);
let mapped = f.niri().layout.windows().next().unwrap().1;
// Now it is committed.
assert!(mapped.is_windowed_fullscreen());
// Disable windowed fullscreen.
f.niri().layout.toggle_windowed_fullscreen(&window_id);
f.double_roundtrip(id);
// Should request without fullscreen state with the tiled size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
let mapped = f.niri().layout.windows().next().unwrap().1;
// Not commited yet.
assert!(mapped.is_windowed_fullscreen());
// Commit in response.
let window = f.client(id).window(&surface);
window.ack_last_and_commit();
f.roundtrip(id);
let mapped = f.niri().layout.windows().next().unwrap().1;
// Now it is committed.
assert!(!mapped.is_windowed_fullscreen());
}
#[test]
fn windowed_fullscreen_chain() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
let mapped = f.niri().layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
f.niri().layout.toggle_windowed_fullscreen(&window_id);
f.roundtrip(id);
f.niri().layout.toggle_windowed_fullscreen(&window_id);
f.roundtrip(id);
f.niri().layout.toggle_windowed_fullscreen(&window_id);
f.roundtrip(id);
f.niri().layout.toggle_windowed_fullscreen(&window_id);
f.double_roundtrip(id);
// Should be four configures matching the four requests.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@r"
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]
"
);
let window = f.client(id).window(&surface);
let serials = Vec::from_iter(
window.configures_received[window.configures_received.len() - 4..]
.iter()
.map(|(s, _c)| *s),
);
let get_state = |f: &mut Fixture| {
let mapped = f.niri().layout.windows().next().unwrap().1;
format!(
"fs {}, wfs {}",
mapped.is_fullscreen(),
mapped.is_windowed_fullscreen()
)
};
let mut states = vec![get_state(&mut f)];
for serial in serials {
let window = f.client(id).window(&surface);
window.xdg_surface.ack_configure(serial);
window.commit();
f.roundtrip(id);
states.push(get_state(&mut f));
}
// We expect fs to always be false (because each Fullscreen state request corresponded to a
// windowed fullscreen), and wfs to toggle on and off.
assert_snapshot!(
states.join("\n"),
@r"
fs false, wfs false
fs false, wfs true
fs false, wfs false
fs false, wfs true
fs false, wfs false
"
);
}
+1
View File
@@ -5,4 +5,5 @@ mod fixture;
mod server;
mod floating;
mod fullscreen;
mod window_opening;
+1
View File
@@ -22,6 +22,7 @@ impl Server {
event_loop.get_signal(),
display,
true,
false,
)
.unwrap();

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