Compare commits

...

223 Commits

Author SHA1 Message Date
Ivan Molodetskikh e05bc269e6 README: Update screenshot 2025-01-11 19:53:25 +03:00
Ivan Molodetskikh d574341f1f wiki: Add missing period 2025-01-11 09:10:45 +03:00
Ivan Molodetskikh 481958f8f7 wiki: Document version string in Packaging 2025-01-11 09:09:25 +03:00
Ivan Molodetskikh 4094469d59 README: Replace next release with version 2025-01-11 09:00:15 +03:00
Ivan Molodetskikh 2261fcb631 wiki: Add Packaging niri page 2025-01-11 08:59:39 +03:00
Ivan Molodetskikh 279c8b6aa2 Back out "rpkg: Print license summary"
This backs out commit 89c991b636.
2025-01-10 17:10:21 +03:00
Ivan Molodetskikh e35c630c1d Format version as calver automatically 2025-01-10 16:37:46 +03:00
Ivan Molodetskikh d3047afa7f rpkg: Set NIRI_BUILD_COMMIT in cargo.toml 2025-01-10 16:19:06 +03:00
Ivan Molodetskikh a03783f54c CI: Add permission to release 2025-01-10 16:04:19 +03:00
Ivan Molodetskikh cbf0d6190d rpkg: Update licenses 2025-01-10 16:02:30 +03:00
Ivan Molodetskikh 89c991b636 rpkg: Print license summary 2025-01-10 15:59:22 +03:00
Ivan Molodetskikh bbbd35e9ef CI: Fix grep check 2025-01-10 15:42:22 +03:00
Ivan Molodetskikh c308be315d wiki: Put version in Since: next release 2025-01-10 15:39:02 +03:00
Ivan Molodetskikh d825e3125e CI: Add a prepare-release workflow 2025-01-10 15:28:50 +03:00
Ivan Molodetskikh 64288de04e rpkg: Use NIRI_BUILD_COMMIT 2025-01-10 15:25:49 +03:00
Ivan Molodetskikh fb4471e69d Add NIRI_BUILD_COMMIT env variable override 2025-01-10 15:20:27 +03:00
Ivan Molodetskikh 8be8694f5f Add NIRI_BUILD_VERSION_STRING env variable to override the version 2025-01-10 15:17:04 +03:00
Ivan Molodetskikh 60b78dc2cd Bump version to 25.01 2025-01-10 15:16:36 +03:00
Ivan Molodetskikh 80fe5a8167 CI: Rearrange some dependencies 2025-01-10 15:15:50 +03:00
dependabot[bot] df58c49876 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 [clap](https://github.com/clap-rs/clap).


Updates `bitflags` from 2.6.0 to 2.7.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.6.0...2.7.0)

Updates `clap` from 4.5.24 to 4.5.26
- [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.24...clap_complete-v4.5.26)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-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-10 11:40:45 +03:00
Ivan Molodetskikh 7dee2f6995 Fix two manual let-else 2025-01-10 09:11:31 +03:00
Ivan Molodetskikh 623687e59b Fix new Clippy warnings 2025-01-10 09:11:31 +03:00
rustn00b 5958d3be62 Allow workspace names to be changed dynamically (#904)
* Add un/set workspace name actions

* Add SetWorkspaceName reference to proptests

* Simplify unname_workspace

* Add ewaf version of set first workspace name test

* Simplify more

* Fix comment

* Make workspace in set-workspace-name a positional option

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-10 06:03:19 +00:00
Ivan Molodetskikh 142e57450d Add missing interactively moved window check in center_window 2025-01-09 11:55:01 +03:00
rustn00b 80815a1591 Add a window swap operation (#899)
Swap the active window with the a neighboring column's active window.


---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
Take into account PR comments

- no longer behave like an expel when a swap is made in a direction
  where there is no column to swap with
- fix janky animation
2025-01-09 08:29:36 +00:00
Ivan Molodetskikh 8412bfb813 Add missing cursor warp when focusing floating/tiling 2025-01-09 10:49:24 +03:00
Ivan Molodetskikh a0f279691a Update dependencies 2025-01-09 10:23:44 +03:00
Ivan Molodetskikh 92aeddb9fe Force-update insta snapshots
1.42.0 reverted a 1.41.0 change to snapshot metadata.
2025-01-09 10:22:39 +03:00
dependabot[bot] d7da88853b build(deps): bump the rust-dependencies group across 1 directory with 4 updates
Bumps the rust-dependencies group with 4 updates in the / directory: [clap](https://github.com/clap-rs/clap), [libdisplay-info](https://github.com/Smithay/libdisplay-info-rs), [serde_json](https://github.com/serde-rs/json) and [insta](https://github.com/mitsuhiko/insta).


Updates `clap` from 4.5.23 to 4.5.24
- [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.23...clap_complete-v4.5.24)

Updates `libdisplay-info` from 0.1.0 to 0.2.2
- [Release notes](https://github.com/Smithay/libdisplay-info-rs/releases)
- [Commits](https://github.com/Smithay/libdisplay-info-rs/compare/v0.1.0...v0.2.2)

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

Updates `insta` from 1.41.1 to 1.42.0
- [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.41.1...1.42.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libdisplay-info
  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-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 08:55:36 +00:00
Frans Skarman 89678c7b1e Set is-active-in-column to true for unmapped windows (#934)
* Set is-active-in-column to true for unmapped windows

* Update wiki/Configuration:-Window-Rules.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-05 13:38:26 +03:00
Ivan Molodetskikh 098c826095 Search for connector duplicates across all devices 2025-01-04 23:32:09 +03:00
Ivan Molodetskikh befbdc3ae5 default-config: Fix typo 2025-01-04 20:56:45 +03:00
Ivan Molodetskikh dca0364f4c Unname connector if a duplicate is detected 2025-01-04 18:03:08 +03:00
Ivan Molodetskikh 37771259d9 Fetch monitor name from EDID only once
Reduce spam when it's unavailable. Assume the name cannot change at runtime;
before if it changed, bad things would probably happen anyway.
2025-01-04 17:56:13 +03:00
Ivan Molodetskikh 4618e4851c Default to unrestricted primary plane scanout 2025-01-04 13:02:22 +03:00
Ivan Molodetskikh b2ca280c49 Restart PipeWire on errors
This lets you restart pipewire and then get a screencast successfully.
2025-01-04 12:23:25 +03:00
Ivan Molodetskikh bf6995f759 CI: Fix MSRV 2025-01-04 11:49:27 +03:00
Ivan Molodetskikh ab0cce7cb7 Add Xrgb/Xbgr to color formats
At least until the scanout check is fixed in Smithay again.
2025-01-04 11:22:56 +03:00
Ivan Molodetskikh 2e422fc026 Update dependencies 2025-01-04 11:22:56 +03:00
Ivan Molodetskikh a2f9d132a0 Migrate to new Rectangle functions 2025-01-04 11:22:56 +03:00
Ivan Molodetskikh 1973b97cc2 Upgrade Smithay (DrmCompositor changes) 2025-01-04 11:22:56 +03:00
Ivan Molodetskikh b3c6f0e661 Add floating binds to the hotkey overlay 2025-01-03 17:26:36 +03:00
Ivan Molodetskikh 6998b17f9e wiki: Update default hotkeys 2025-01-03 17:23:29 +03:00
Ivan Molodetskikh ed9932d70d wiki: Update the layer-shell components page 2025-01-03 17:02:18 +03:00
Ivan Molodetskikh a5f3b2a949 Clear on-demand layer-shell focus in more cases 2025-01-03 17:00:13 +03:00
Ivan Molodetskikh 152ed59502 Allow keyboard focus for bottom and background layers 2025-01-03 16:41:39 +03:00
Ivan Molodetskikh 8e16be9e11 Allow pop-up grabs for bottom and background layers 2025-01-03 16:24:23 +03:00
Ivan Molodetskikh 300701f44e Render layer-shell pop-ups on top 2025-01-03 15:57:59 +03:00
Ivan Molodetskikh d1370622d8 wiki: Expand application issues a bit 2025-01-03 12:30:17 +03:00
Ivan Molodetskikh 0134166009 README: Expand Status a bit 2025-01-03 11:11:11 +03:00
Ivan Molodetskikh ddb9084260 wiki/Xwayland: Add a labwc section 2025-01-03 10:50:02 +03:00
Ivan Molodetskikh 0224452cef wiki/Xwayland: Clarify xwayland-satellite 2025-01-03 10:50:02 +03:00
Julian Schuler c17d4dc050 Add actions to focus/move to next/previous monitor 2025-01-02 15:15:23 +03:00
bbb651 4e33f45522 Add Mouse{Left,Right,Middle,Back,Forward} binds 2025-01-02 14:59:15 +03:00
Christian Meissl b16d7abb35 skip keyboard focus for layer shell surfaces not...
...requesting keyboard interactivity
2025-01-02 14:24:39 +03:00
Christian Meissl 2f17a30157 xdg: do not focus unmapped popup on grab
a grab is requested for an unmapped popup,
delay focusing the popup until the first keyboard
interaction
2025-01-02 14:24:39 +03:00
Ivan Molodetskikh 0dbd14ebdc Update dependencies 2025-01-02 11:50:51 +03:00
Ivan Molodetskikh 8b3d8ccb47 Update dependabot.yml 2025-01-02 11:34:04 +03:00
Ivan Molodetskikh f8ff2e4e28 Update dependabot.yml 2025-01-02 11:32:34 +03:00
Ivan Molodetskikh 044f0d41a5 Update dependabot.yml 2025-01-02 11:31:37 +03:00
Ivan Molodetskikh 4089bebd83 Create dependabot.yml 2025-01-02 11:30:01 +03:00
Ivan Molodetskikh d4787c75fd Delete dependabot.yml 2025-01-02 11:26:49 +03:00
Ivan Molodetskikh 3bf0a57b82 Create dependabot.yml 2025-01-02 11:20:47 +03:00
Ivan Molodetskikh cc505ae49f Delete dependabot.yml
Let's see if re-creating fixes it.
2025-01-02 11:19:20 +03:00
dependabot[bot] 2f6de136dd build(deps): bump the rust-dependencies group with 19 updates
Bumps the rust-dependencies group with 19 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.93` | `1.0.95` |
| [bytemuck](https://github.com/Lokathor/bytemuck) | `1.19.0` | `1.21.0` |
| [calloop](https://github.com/Smithay/calloop) | `0.14.1` | `0.14.2` |
| [clap](https://github.com/clap-rs/clap) | `4.5.20` | `4.5.23` |
| [fastrand](https://github.com/smol-rs/fastrand) | `2.2.0` | `2.3.0` |
| [libc](https://github.com/rust-lang/libc) | `0.2.162` | `0.2.169` |
| [ordered-float](https://github.com/reem/rust-ordered-float) | `4.5.0` | `4.6.0` |
| [pango](https://github.com/gtk-rs/gtk-rs-core) | `0.20.4` | `0.20.7` |
| [pangocairo](https://github.com/gtk-rs/gtk-rs-core) | `0.20.4` | `0.20.7` |
| [png](https://github.com/image-rs/image-png) | `0.17.14` | `0.17.16` |
| [portable-atomic](https://github.com/taiki-e/portable-atomic) | `1.9.0` | `1.10.0` |
| [serde](https://github.com/serde-rs/serde) | `1.0.214` | `1.0.217` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.132` | `1.0.134` |
| [tracing](https://github.com/tokio-rs/tracing) | `0.1.40` | `0.1.41` |
| [tracy-client](https://github.com/nagisa/rust_tracy_client) | `0.17.4` | `0.17.6` |
| [url](https://github.com/servo/rust-url) | `2.5.3` | `2.5.4` |
| [proptest](https://github.com/proptest-rs/proptest) | `1.5.0` | `1.6.0` |
| [proptest-derive](https://github.com/proptest-rs/proptest) | `0.5.0` | `0.5.1` |
| [xshell](https://github.com/matklad/xshell) | `0.2.6` | `0.2.7` |


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

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

Updates `calloop` from 0.14.1 to 0.14.2
- [Release notes](https://github.com/Smithay/calloop/releases)
- [Changelog](https://github.com/Smithay/calloop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Smithay/calloop/compare/v0.14.1...v0.14.2)

Updates `clap` from 4.5.20 to 4.5.23
- [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.20...clap_complete-v4.5.23)

Updates `fastrand` from 2.2.0 to 2.3.0
- [Release notes](https://github.com/smol-rs/fastrand/releases)
- [Changelog](https://github.com/smol-rs/fastrand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/fastrand/compare/v2.2.0...v2.3.0)

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

Updates `ordered-float` from 4.5.0 to 4.6.0
- [Release notes](https://github.com/reem/rust-ordered-float/releases)
- [Commits](https://github.com/reem/rust-ordered-float/compare/v4.5.0...v4.6.0)

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

Updates `pangocairo` from 0.20.4 to 0.20.7
- [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.4...0.20.7)

Updates `png` from 0.17.14 to 0.17.16
- [Changelog](https://github.com/image-rs/image-png/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image-png/compare/v0.17.14...v0.17.16)

Updates `portable-atomic` from 1.9.0 to 1.10.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.9.0...v1.10.0)

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

Updates `serde_json` from 1.0.132 to 1.0.134
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.132...v1.0.134)

Updates `tracing` from 0.1.40 to 0.1.41
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.40...tracing-0.1.41)

Updates `tracy-client` from 0.17.4 to 0.17.6
- [Commits](https://github.com/nagisa/rust_tracy_client/compare/tracy-client-v0.17.4...tracy-client-v0.17.6)

Updates `url` from 2.5.3 to 2.5.4
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.3...v2.5.4)

Updates `proptest` from 1.5.0 to 1.6.0
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/commits)

Updates `proptest-derive` from 0.5.0 to 0.5.1
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/0.5.1/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/proptest-derive-0.5.0...0.5.1)

Updates `xshell` from 0.2.6 to 0.2.7
- [Changelog](https://github.com/matklad/xshell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/xshell/compare/v0.2.6...v0.2.7)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bytemuck
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: calloop
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: fastrand
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ordered-float
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: pango
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: pangocairo
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: png
  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: 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
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracy-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: proptest
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: proptest-derive
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: xshell
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 11:15:11 +03:00
dependabot[bot] da21b50137 build(deps): bump smithay-drm-extras from c3f3ac8 to 5186cf7
Bumps [smithay-drm-extras](https://github.com/Smithay/smithay) from `c3f3ac8` to `5186cf7`.
- [Release notes](https://github.com/Smithay/smithay/releases)
- [Commits](https://github.com/Smithay/smithay/compare/c3f3ac8dc0776d47bc50f9a1911b613a56e6e04b...5186cf7dec2472a91e3c248772954b1141dab7f2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 10:59:24 +03:00
Ivan Molodetskikh a38a5c529f Create dependabot.yml
Copied from Helix.
2025-01-02 10:44:27 +03:00
Ivan Molodetskikh 44b5612697 Remove notify-rust dependency
It uses outdated zbus.
2025-01-02 09:33:54 +03:00
bbb651 0113292cf6 Upgrade zbus and async-io 2025-01-02 08:50:48 +03:00
Ivan Molodetskikh 4741ab2e04 spec: Set XDG_RUNTIME_DIR for tests 2024-12-30 22:15:44 +03:00
Ivan Molodetskikh 08fb9435fd Fix width shrinking when going from floating to scrolling 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 793e92e9d6 Add default-floating-position relative-to property 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh a7c57f4faf Add toggle-window-width by-id action 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 8409107a5b Implement default-window-height for scrolling windows 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 9089c3fb02 Fix move-window-to-workspace panic when wrong monitor is active 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 6c897d5201 Add center-window by-id action 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 6cb5135f34 Clamp single tiled window height
Now that we have floating for taller-than-screen windows.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 44bf45794e Dump post-unfullscreen configure in snapshot tests 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh d6da9f47d8 tests: Respond to post-initial configures 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh be05b66ac3 Hide focus ring for unfocused layout and under interactive move 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh d1998ae3fa Disable double-resize-click for floating windows 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 3c2e1554c6 Add default-floating-position window rule 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 744955ba69 floating: Remove initial offset when always-centering 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 7af33f9e6a wiki: Add some floating window documentation 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 3c0705b0ae Implement buffer delta for toplevels 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 4ea4d2bd3b layout: Add animate arg to move_floating_window() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 6c52077d92 Add move-floating-window action 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 73bf7b1730 Allow hyphen values for set-window-width/height arg
Make args like set-window-height -10 parse as is, without having to insert a --.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh b394cb6379 floating: Cancel resize when moving or changing size 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 60854e180e Add is_floating to Window IPC 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 5b4750a009 Add focus-floating/tiling actions 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh ad50dd21fe Add move-window-to-floating/tiling actions 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 8b0cb0bb57 Add set-window-width action 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh a24a6e4e3c Implement is-floating window rule matcher 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 6fba4c371e Implement default-window-height window rule
Only works for floats that aren't initially fullscreen atm.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 27911431db tests: Rename DefaultWidth to DefaultSize 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh db6447ed79 floating: Support default-column-width in most cases
open-fullscreen + open-floating default width is still not supported in this
commit.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 99c0fabee6 layout: Use new helper function 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh fc99724aba Add open-focused window rule 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 88fbc62b1d wiki: Update Firefox window rules to match non-Flatpak 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e8027d571f layout: Implement next-to + open-fullscreen 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh daaee43be3 layout: Refactor window opening targets 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 0d71cb93af Add window opening size client-server tests 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e5e50e82d5 wiki: Clarify that preset width doesn't take borders into account only in tiling 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 7e852124a5 floating: Fix window position constraining with non-zero working area loc 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh f66a49bc42 floating: Constrain popups to working area 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh baf78ccda2 floating: Remove TODO on tile removing width 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 31f0e66f45 floating: Comment on toggle-full-width status 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 28b78a563b layout: Pass and store view_size on a Tile 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 2f380de73b floating: Take into account non-fixed min/max size window rule 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e3a9a39c9a floating: Implement the rest of set-window-width/height 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 1710bb78df floating: Implement toggle-width/height actions 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 3e13fc3e70 floating: Change from getters to pub(super)
These fields are just data storage. They won't have any logic in
getters/setters.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh befc399506 default-config: Make Firefox PiP floating 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 88116b9fb1 Preserve tile when moving across monitors 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 53e1c58cc5 Remember floating window position 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 4b9ecdd11d Render fullscreen scrolling windows on top of floating 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e31e409ee8 tests: Fix spelling mistake in wfs Display 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 5488aaf69f floating: Don't use fullscreen size as floating size 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 96e493d8b1 Restore floating size during interactive move 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e409453fbd floating: Update stored size only on removal 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 309bf1348c floating: Improve expected size requests to avoid (0, 0) and duplicates 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 76a5635298 layout: Preserve the Tile when moving across workspaces 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh f4f2a1f6de floating: Remember and restore window size 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh a440805ea1 Add floating sizing tests 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh c359672bd2 floating: Request size only once
Let floating windows resize themselves and keep that size.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 38350935e6 layout: Rename update_interactive_resize() to on_commit() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 421cd89a0f layout: Accept &mut self in request_fullscreen() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 5ce3369aa6 layout: Support fullscreen for auto-floating windows 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh f38acfe988 layout: Remember whether to unfullscreen back into floating 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 965619d096 layout: Move toggle_fullscreen() impl to Workspace 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 9f017e834c wiki: Document new floating window rule and gesture 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 3c67b08488 floating: Implement directional move 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 4add755a4d layout/floating: Extract move_and_animate() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 56e249aee6 floating: Implement center_window() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 6a7c8fcfd5 floating: Implement directional focus 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 14b1003c62 layout: Implement focus_right_or_first() generically 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 43a4bae010 Extract center_preferring_top_left_in_area() 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 9c205f77a2 layout/floating: Move a function higher up
Let's group action functions together. Activate is an action and set
width/height too.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh c2e4cfd832 Stub out actions when floating is active
Make sure they don't go to the unfocused scrolling layout at least.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh c008e1c5bc floating: Implement smarter clamping for window location
A small part of the window always remains on-screen regardless of the working
area changes.

Interactive move lets the user position the window anywhere; automatic actions
like toggle-window-floating and dialog opening try to put the window fully
on-screen.

The size-fraction canonical floating window position remains unclamped, and
clamping happens when recomputing the logical position.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 1aa60f0da3 Make right click during move toggle floating 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh bd1fd8383c Stop move grab when the start button is released
Rather than when all buttons are released.
2024-12-30 20:12:37 +03:00
Ivan Molodetskikh aac54d0ea1 Implement floating child stacking above parents 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 4fe718581b layout: Extract TestWindowParams 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 71842f07bd Make interactive move keep in the same layout (floating/tiling) 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh f2bec1f82f Always honor min height in new window size 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 10460191b9 Honor min/max size in more places like initial configure 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh c5fffd6e2c Initial WIP floating window implementation 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 951f63b6fd temp: Use patched Smithay (fix VRR cursor-plane-only) 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh e6d8932b3b Update for Smithay VRR changes 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 70f96cca0a Update Smithay (presentation-time v2) 2024-12-30 20:12:37 +03:00
Ivan Molodetskikh 4e357e9659 config: Fix border rule on -> off merging 2024-12-27 15:42:56 +03:00
Ivan Molodetskikh 1f8aed6732 config: Add a test for border rule on/off merging 2024-12-27 15:42:55 +03:00
Maximilian Huber fa2bace3cd Fix nix flake for client-server tests (#896)
This was suggested by @sodiboo in
https://github.com/YaLTeR/niri/issues/894#issuecomment-2562153840 and
was copied from https://github.com/sodiboo/niri-flake/commit/350e6b68c70f5002a75e10521f5e66ace4b5eed1i

Signed-off-by: Maximilian Huber <gh@maxhbr.de>
2024-12-26 14:44:07 +00:00
Nathan 955039b5ea Update Configuration:-Key-Bindings.md (#893)
* Update Configuration:-Key-Bindings.md

Added Leve5 notes with scant instruction on how to use.

* Update wiki/Configuration:-Key-Bindings.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-12-23 21:08:56 +03:00
Ivan Molodetskikh 771ea1e815 Implement client-server test infra and window opening tests
These tests make a real Niri instance and real Wayland clients (via manual
wayland-rs implementation), both on the same event loop local to the test. This
allows testing the full Wayland interaction, including arbitrary event ordering
and delays.

To start off, add a massive powerset test for the settings that influence where
a window may open.
2024-12-22 15:19:46 +03:00
Ivan Molodetskikh d38bfc4aff Add test-only single-pixel-buffer support 2024-12-22 15:19:46 +03:00
Ivan Molodetskikh fbb0054232 Add a Headless backend for tests
Rendering and stuff is unimplemented.
2024-12-22 15:19:46 +03:00
Ivan Molodetskikh 2d3c36edae Switch from k9 to insta for snapshot testing
We'll need some advanced features from insta.
2024-12-22 15:19:46 +03:00
Ivan Molodetskikh 8dcc41a54d Initialize PipeWire lazily
This helps with:
- System setups starting PipeWire late (after niri startup, but before any
  screencast).
- Tests which don't even want to start PipeWire.
2024-12-22 15:19:46 +03:00
bbb651 ba3d2e36c8 Bump MSRV to 1.80
It should be old enough for most distros, and allows upgrading to `zbus 5.x`
2024-12-22 15:19:46 +03:00
bbb651 b51047ffcc Avoid implicit feature names 2024-12-22 15:19:46 +03:00
Rémi Labeyrie b1c40a9079 fix: check for layer surface under cursor when clicking 2024-12-22 15:13:17 +03:00
Ivan Molodetskikh b014c267ae README: Replace Matrix badge with static
The dynamic one broke recently.
2024-12-20 23:07:19 +03:00
Ivan Molodetskikh 6b16cc52db Add force-pipewire-invalid-modifier debug flag 2024-12-17 17:08:14 +03:00
Ivan Molodetskikh d35ad73e35 wiki: Change Since 0.1.11 to Since next release 2024-12-15 16:44:35 +03:00
Ivan Molodetskikh 2a1af3d9ae Add missing blank line 2024-12-15 10:40:09 +03:00
Ivan Molodetskikh 82e30246c1 Use gtk Notification portal
xdg-gnome 47 now implements notifications via GNOME Shell API which we don't
have. So force the gtk portal to make notifications work again.
2024-12-11 21:39:58 +03:00
Salman Farooq bb3a05bb3f Activate monitors on session unlock (#858)
So that e.g. unlocking by touching the fingerprint reader powers on the monitors.

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
Co-authored-by: Salman Farooq <46742354+SalmanFarooqShiekh@users.noreply.github.com>
2024-12-11 03:53:41 -08:00
Ivan Molodetskikh 40fa82275c Extract rules.apply_{min,max}_size() 2024-12-09 13:25:52 +03:00
Ivan Molodetskikh 9824321fc9 layout: Return instead of breaking
There's no code past this, and we want to break out of all loops.
2024-12-08 09:25:39 +03:00
Ivan Molodetskikh 27e607ab82 layout: Return bool from activate_window()
Avoid an extra has_window() call.
2024-12-08 09:25:27 +03:00
Ivan Molodetskikh a2b27b8790 layout: Ignore more actions during interactive move
The interactively moved window is the active window, so this makes sense.
2024-12-07 19:38:48 +03:00
Ivan Molodetskikh 396089ef0e layout: Extract Tile::verify_invariants() 2024-12-07 19:38:48 +03:00
Ivan Molodetskikh df98b5021d layout: Mark accessors as cfg(test) 2024-12-07 19:38:48 +03:00
sodiboo 34ce6d0b02 nix: update flake.lock 2024-12-03 05:53:48 -08:00
sodiboo 7af937b08e nix: clang -> rustPlatform.bindgenHook 2024-12-03 05:53:48 -08:00
Ivan Molodetskikh 8665003269 layout: Extract ScrollingSpace
Leave the Workspace to do the workspace parts, and extract the scrolling parts
into a new file. This is a pre-requisite for things like the floating layer
(which will live in a workspace alongside the scrolling layer).

As part of this huge refactor, I found and fixed at least these issues:
- Wrong horizontal popup unconstraining for a smaller window in an
  always-centered column.
- Wrong workspace switch in focus_up_or_right().
2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 1e76716819 layout: Add a test for windows on other workspace remaining activated 2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 91a42fdf58 layout: Fix windows on other workspaces losing activated state
This erroneous check was introduced in interactive move.
2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 5ed5243be6 layout: Fix possible crash when dropping move on different, animating output 2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 4560251e64 layout: Correct variable names 2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 2020dca3e0 layout: Use tiles_mut() in Workspace::clear_unmap_snapshot() 2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 7fc2121454 layout: Extract Workspace::tiles() 2024-12-01 22:24:21 -08:00
Ivan Molodetskikh 8b84afbd38 Add strict-new-window-focus-policy debug flag 2024-11-29 21:57:36 -08:00
Christian Meissl 305fc3b557 Activate newly mapped windows with a valid activation token
most of the time the activation token is passed
while the window is still unmapped. in this case
store the intend to activate the window for
later retrieval on map.
2024-11-29 21:57:36 -08:00
Christian Meissl 61f2ac01d7 xdg: startup activation
pass an activation token to process spawned through actions
2024-11-29 21:57:36 -08:00
Ivan Molodetskikh 39a9f55205 Fix new warnings 2024-11-29 09:33:08 +03:00
FluxTape 11f351dbeb Implement empty-workspace-above-first (#745)
* Implement empty-workspace-above-first option

* add two failing tests

* fix interactive_move_onto_empty_output_ewaf and
interactive_move_onto_first_empty_workspace tests

* Add two failing ewaf option toggle tests

* Fix adding/removing first empty workspace on option toggle

* Don't remove first empty workspace if focused

* Stop workspace switch when enabling ewaf

* layout/monitor: Offset workspace switch on adding workspace above

* Fix some initial active workspace ids with ewaf

* wiki: Document empty-workspace-above-first

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-29 08:46:13 +03:00
Ivan Molodetskikh 815fa379ea layout: Stop workspace switch when moving workspaces to primary
Okay, this might be one of the oldest layout issues to have remained uncaught.
Well, maybe as I add more randomized tests, I'll catch even more of those.
2024-11-27 20:55:20 +03:00
Ivan Molodetskikh 4c480a1ea3 layout/tests: Add post option update to randomized test
Will help to catch cases where updating options doesn't update the state
correctly.
2024-11-26 22:02:46 +03:00
Ivan Molodetskikh fa4aa0e06d layout: Fix adjusting for scale for moved tile when reloading config 2024-11-26 22:01:26 +03:00
Ivan Molodetskikh e2a6374bf5 layout/tests: Return Layout from check_ops()
Cuts down on boilerplate in a few places.
2024-11-26 22:00:44 +03:00
Ivan Molodetskikh dc14554053 layout: Extract update_options() 2024-11-26 21:59:05 +03:00
Ivan Molodetskikh 985ca7b643 layout/tests: Allow AddWindowRightOf interactive moved window
Guess I forgot this.
2024-11-26 15:24:28 +03:00
Ivan Molodetskikh 60624d64fa layout/tests: Standardize on usize for output id in tests 2024-11-26 15:24:28 +03:00
Ivan Molodetskikh 2935dae89e wiki: Add animation timing page 2024-11-25 04:07:59 -08:00
Ivan Molodetskikh 4c22c3285d Refactor animation timing to use lazy clocks 2024-11-25 04:07:59 -08:00
Ivan Molodetskikh 93cee2994a Refactor animations to take explicit current time 2024-11-25 04:07:59 -08:00
Ivan Molodetskikh 9c7e8d04d2 Extract Niri::advance_animations() 2024-11-23 15:09:16 +03:00
Ivan Molodetskikh 1e6b8906e0 layout/monitor: Extract add_workspace_bottom() 2024-11-23 15:07:52 +03:00
Ivan Molodetskikh 6c5b92e5c0 Add interactive_move_onto_empty_output test
Tests the add_workspace_bottom() in Monitor::add_tile().
2024-11-23 15:07:35 +03:00
Ivan Molodetskikh 38c515e12e pw: Fix potential crash when disconnecting output 2024-11-23 15:07:09 +03:00
Ivan Molodetskikh c239937fac Focus target window/output on DnD
In sway, focus-follows-mouse keeps working during DnD, but not in niri.
So it can be surprising when you DnD something into another app, but it
doesn't get automatically focused. This commit fixes that.

Even if the DnD is not validated, or if there's no target surface (e.g.
dropped on the niri background), focus the target output, since that's
how Firefox's drag-tab-into-new-window works for example.
2024-11-22 09:37:26 +03:00
Ivan Molodetskikh bafa574784 wiki: Link layer rules from block-out-from window rules 2024-11-21 14:57:41 +03:00
Ivan Molodetskikh 199a5854a8 wiki: Add Since to layer rules 2024-11-21 14:57:41 +03:00
Ridan Vandenbergh a74a578198 Add focus-window-previous action (#811)
* Add `FocusWindowPrevious` action

* remove [`

* track previous focus in Niri instead of every window

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-21 14:48:51 +03:00
Ivan Molodetskikh 7de752ec56 Bump CI image versions 2024-11-20 13:16:02 +03:00
Ivan Molodetskikh 0a833171ac Update Smithay (popup grab fix) 2024-11-20 08:34:19 +03:00
Ivan Molodetskikh 1a0612cbfd Implement layer rules: opacity and block-out-from 2024-11-14 12:05:30 +03:00
Ivan Molodetskikh fbbd3ba349 niri: Extract render_layer() 2024-11-14 10:24:04 +03:00
Ivan Molodetskikh 1028639186 config: Add RegexEq util type instead of manual PartialEq 2024-11-14 09:44:07 +03:00
Ivan Molodetskikh 0e5e764c78 Add niri msg layers 2024-11-12 21:44:00 +03:00
Ivan Molodetskikh db1faecc95 Guard against closed screenshot UI in its binds
They can trigger with closed screenshot UI via key repeat.
2024-11-12 19:26:44 +03:00
Ivan Molodetskikh c2c415d2e8 wiki/sidebar: Update application issues title 2024-11-12 10:11:41 +03:00
Ivan Molodetskikh d193928f31 Add PID to Window IPC 2024-11-12 09:37:25 +03:00
Ivan Molodetskikh 17861e0003 Change expel-window-from-column to expel the bottom window
This way, expel becomes symmetric with consume. This is also how it
works in PaperWM. Though, in PaperWM if the expelled window was focused,
it will remain focused, while in this commit it is never focused, making
it the exact opposite of consume.

Use consume-or-expel-window-right for the old expel behavior.
2024-11-11 18:07:41 +03:00
Ivan Molodetskikh 97fe964e00 Make consume-or-expel binds more prominent
I find myself using them much more than regular consume or expel.
2024-11-11 17:56:35 +03:00
Ivan Molodetskikh 9debb5db23 wiki: Mention Ghidra in application issues 2024-11-11 10:06:23 +03:00
Ramses 494b438151 Unhide the pointer on scroll events (#797)
* Unhide the pointer on scroll events

Since we reset the surface under the pointer when we hide the pointer
(see update_pointer_contents), scroll events don't work when the pointer
is hidden.
So to make scrolling work, we make sure that we unhide the pointer when
a scrolling event occurs.

* Update src/input/mod.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-11 06:08:29 +00:00
Ivan Molodetskikh 010a236882 Start interactive move on Mod+Touch 2024-11-10 09:47:03 +03:00
Ivan Molodetskikh 1951d2a9f2 Fix scrolling not working with missing mouse config 2024-11-10 09:14:22 +03:00
3252 changed files with 57470 additions and 7001 deletions
+22
View File
@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
groups:
smithay:
patterns:
- "smithay"
- "smithay-drm-extras"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
+22 -26
View File
@@ -23,8 +23,7 @@ jobs:
release-flag: '--release'
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-22.04
container: ubuntu:23.10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -33,8 +32,8 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
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 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
- uses: dtolnay/rust-toolchain@stable
@@ -63,7 +62,7 @@ jobs:
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build Tests
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
@@ -74,8 +73,7 @@ jobs:
fail-fast: false
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -84,8 +82,8 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
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 libadwaita-1-dev libdisplay-info-dev
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
- uses: dtolnay/rust-toolchain@stable
@@ -98,9 +96,8 @@ jobs:
strategy:
fail-fast: false
name: 'msrv - 1.77.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
name: 'msrv - 1.80.1'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -109,10 +106,10 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
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 libadwaita-1-dev libdisplay-info-dev
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
- uses: dtolnay/rust-toolchain@1.77.0
- uses: dtolnay/rust-toolchain@1.80.1
- uses: Swatinem/rust-cache@v2
@@ -123,8 +120,7 @@ jobs:
fail-fast: false
name: clippy
runs-on: ubuntu-22.04
container: ubuntu:23.10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -133,8 +129,8 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
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 libadwaita-1-dev libdisplay-info-dev
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
- uses: dtolnay/rust-toolchain@stable
with:
@@ -146,7 +142,7 @@ jobs:
run: cargo clippy --all --all-targets
rustfmt:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -161,8 +157,8 @@ jobs:
run: cargo fmt --all -- --check
fedora:
runs-on: ubuntu-22.04
container: fedora:39
runs-on: ubuntu-24.04
container: fedora:41
steps:
- uses: actions/checkout@v4
@@ -172,13 +168,13 @@ 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 libadwaita-devel libdisplay-info-devel
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
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
nix:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -200,7 +196,7 @@ jobs:
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -212,7 +208,7 @@ jobs:
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
+63
View File
@@ -0,0 +1,63 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
version:
description: 'Public version'
required: true
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
RUN_SLOW_TESTS: 1
jobs:
prepare-release:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
- 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
- uses: dtolnay/rust-toolchain@stable
- name: Create vendored dependencies archive
run: |
mkdir .cargo
cargo vendor --locked > .cargo/config.toml
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
- name: Build
run: cargo build --all --frozen --release
- name: Build tests
run: cargo test --no-run --all --frozen --release
- name: Test
run: cargo test --all --frozen --release -- --nocapture
- name: Draft release
uses: softprops/action-gh-release@v2
with:
draft: true
tag_name: v${{ github.event.inputs.version }}
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
fail_on_unmatched_files: true
Generated
+596 -905
View File
File diff suppressed because it is too large Load Diff
+44 -34
View File
@@ -1,25 +1,29 @@
[workspace]
members = ["niri-visual-tests"]
members = [
"niri-config",
"niri-ipc",
"niri-visual-tests",
]
[workspace.package]
version = "0.1.10"
version = "25.1.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.77"
rust-version = "1.80"
[workspace.dependencies]
anyhow = "1.0.93"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.4", default-features = false }
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"
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]
git = "https://github.com/Smithay/smithay.git"
@@ -47,32 +51,31 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "1.13.0", optional = true }
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.19.0", features = ["derive"] }
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
bytemuck = { version = "1.21.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.9.0"
fastrand = "2.2.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"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.162"
libdisplay-info = "0.1.0"
libc = "0.2.169"
libdisplay-info = "0.2.2"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.10", path = "niri-config" }
niri-ipc = { version = "0.1.10", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.5.0"
pango = { version = "0.20.4", features = ["v1_44"] }
pangocairo = "0.20.4"
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"] }
pangocairo = "0.20.7"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.14"
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
png = "0.17.16"
portable-atomic = { version = "1.10.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.3"
serde.workspace = true
@@ -81,11 +84,11 @@ smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.3", optional = true }
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
xcursor = "0.3.8"
zbus = { version = "~3.15.2", optional = true }
zbus = { version = "5.2.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -107,15 +110,18 @@ features = [
[dev-dependencies]
approx = "0.5.1"
k9.workspace = true
proptest = "1.5.0"
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
xshell = "0.2.6"
calloop-wayland-source = "0.4.0"
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"
xshell = "0.2.7"
[features]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-io", "notify-rust", "url"]
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
@@ -138,8 +144,12 @@ lto = "thin"
# knuffel with chomsky generates a metric ton of debuginfo.
debug = false
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "0.1.10"
version = "25.01"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+33 -5
View File
@@ -1,7 +1,7 @@
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
@@ -10,7 +10,7 @@
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
![niri with a few windows open](https://github.com/user-attachments/assets/d142e57d-a25d-4ddb-ab46-311417458211)
## About
@@ -28,7 +28,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Scrollable tiling
- Built from the ground up for scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
@@ -45,10 +45,35 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Here are some points you may have questions about:
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
- **NVIDIA**: seems to work fine.
- **Floating windows**: yes, starting from niri 25.01.
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
We have touchpad gestures, but no touchscreen gestures yet.
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
- Steam and games, including Proton: work perfectly through xwayland-satellite.
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
- Discord and other Electron apps: work well through xwayland-satellite.
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
@@ -78,3 +103,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[gamescope]: https://github.com/ValveSoftware/gamescope
Generated
+9 -9
View File
@@ -2,11 +2,11 @@
"nodes": {
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"lastModified": 1733064805,
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1727663505,
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
"lastModified": 1733106880,
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
"type": "github"
},
"original": {
+11 -8
View File
@@ -26,10 +26,8 @@
{
lib,
cairo,
clang,
dbus,
libGL,
libclang,
libdisplay-info,
libinput,
seatd,
@@ -79,7 +77,7 @@
strictDeps = true;
nativeBuildInputs = [
clang
rustPlatform.bindgenHook
pkg-config
];
@@ -108,6 +106,15 @@
++ lib.optional withSystemd "systemd";
buildNoDefaultFeatures = true;
# ever since this commit:
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
# and thus creates a real socket file in the runtime dir.
# this is fine for our build, we just need to make sure it has a directory to write to.
preCheck = ''
export XDG_RUNTIME_DIR="$(mktemp -d)"
'';
postInstall =
''
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
@@ -119,8 +126,6 @@
'';
env = {
LIBCLANG_PATH = lib.getLib libclang + "/lib";
# Force linking with libEGL and libwayland-client
# so they can be discovered by `dlopen()`
RUSTFLAGS = toString (
@@ -191,7 +196,7 @@
];
nativeBuildInputs = [
pkgs.clang
pkgs.rustPlatform.bindgenHook
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
@@ -201,8 +206,6 @@
];
env = {
inherit (niri) LIBCLANG_PATH;
# WARN: Do not overwrite this variable in your shell!
# It is required for `dlopen()` to work on some libraries; see the comment
# in the package expression
+2 -2
View File
@@ -12,13 +12,13 @@ bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.10", path = "../niri-ipc" }
niri-ipc = { version = "25.1.0", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
k9.workspace = true
insta.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
+22
View File
@@ -0,0 +1,22 @@
use crate::{BlockOutFrom, RegexEq};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
#[knuffel(children(name = "match"))]
pub matches: Vec<Match>,
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
+324 -45
View File
@@ -10,9 +10,12 @@ use std::time::Duration;
use bitflags::bitflags;
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use layer_rule::LayerRule;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
use regex::Regex;
use niri_ipc::{
ConfiguredMode, LayoutSwitchTarget, PositionChange, SizeChange, Transform,
WorkspaceReferenceArg,
};
use smithay::backend::renderer::Color32F;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@@ -21,6 +24,11 @@ use smithay::reexports::input;
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);
pub mod layer_rule;
mod utils;
pub use utils::RegexEq;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
#[knuffel(child, default)]
@@ -51,6 +59,8 @@ pub struct Config {
pub environment: Environment,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
#[knuffel(children(name = "layer-rule"))]
pub layer_rules: Vec<LayerRule>,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
@@ -188,8 +198,8 @@ pub struct Touchpad {
pub disabled_on_external_mouse: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child, unwrap(argument), default = FloatOrInt(1.0))]
pub scroll_factor: FloatOrInt<0, 100>,
#[knuffel(child, unwrap(argument))]
pub scroll_factor: Option<FloatOrInt<0, 100>>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -210,8 +220,8 @@ pub struct Mouse {
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child, unwrap(argument), default = FloatOrInt(1.0))]
pub scroll_factor: FloatOrInt<0, 100>,
#[knuffel(child, unwrap(argument))]
pub scroll_factor: Option<FloatOrInt<0, 100>>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -437,6 +447,8 @@ pub struct Layout {
pub center_focused_column: CenterFocusedColumn,
#[knuffel(child)]
pub always_center_single_column: bool,
#[knuffel(child)]
pub empty_workspace_above_first: bool,
#[knuffel(child, unwrap(argument), default = Self::default().gaps)]
pub gaps: FloatOrInt<0, 65535>,
#[knuffel(child, default)]
@@ -453,6 +465,7 @@ impl Default for Layout {
default_column_width: Default::default(),
center_focused_column: Default::default(),
always_center_single_column: false,
empty_workspace_above_first: false,
gaps: FloatOrInt(16.),
struts: Default::default(),
preset_window_heights: Default::default(),
@@ -695,7 +708,7 @@ pub enum PresetSize {
Fixed(#[knuffel(argument)] i32),
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DefaultPresetSize(pub Option<PresetSize>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
@@ -965,6 +978,8 @@ pub struct WindowRule {
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child)]
pub default_window_height: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child, unwrap(argument))]
@@ -973,6 +988,10 @@ pub struct WindowRule {
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_floating: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_focused: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
@@ -1000,15 +1019,16 @@ pub struct WindowRule {
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, unwrap(argument))]
pub variable_refresh_rate: Option<bool>,
#[knuffel(child)]
pub default_floating_position: Option<FoIPosition>,
}
// Remember to update the PartialEq impl when adding fields!
#[derive(knuffel::Decode, Debug, Default, Clone)]
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub app_id: Option<Regex>,
pub app_id: Option<RegexEq>,
#[knuffel(property, str)]
pub title: Option<Regex>,
pub title: Option<RegexEq>,
#[knuffel(property)]
pub is_active: Option<bool>,
#[knuffel(property)]
@@ -1016,20 +1036,11 @@ pub struct Match {
#[knuffel(property)]
pub is_active_in_column: Option<bool>,
#[knuffel(property)]
pub is_floating: Option<bool>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
impl PartialEq for Match {
fn eq(&self, other: &Self) -> bool {
self.is_active == other.is_active
&& self.is_focused == other.is_focused
&& self.is_active_in_column == other.is_active_in_column
&& self.at_startup == other.at_startup
&& self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str)
&& self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct CornerRadius {
pub top_left: f32,
@@ -1073,6 +1084,25 @@ pub struct BorderRule {
pub inactive_gradient: Option<Gradient>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FoIPosition {
#[knuffel(property)]
pub x: FloatOrInt<-65535, 65535>,
#[knuffel(property)]
pub y: FloatOrInt<-65535, 65535>,
#[knuffel(property, default)]
pub relative_to: RelativeTo,
}
#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum RelativeTo {
#[default]
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Default, PartialEq)]
pub struct Binds(pub Vec<Bind>);
@@ -1094,6 +1124,11 @@ pub struct Key {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Trigger {
Keysym(Keysym),
MouseLeft,
MouseRight,
MouseMiddle,
MouseBack,
MouseForward,
WheelScrollDown,
WheelScrollUp,
WheelScrollLeft,
@@ -1168,6 +1203,7 @@ pub enum Action {
FullscreenWindowById(u64),
#[knuffel(skip)]
FocusWindow(u64),
FocusWindowPrevious,
FocusColumnLeft,
FocusColumnRight,
FocusColumnFirst,
@@ -1204,7 +1240,12 @@ pub enum Action {
ConsumeOrExpelWindowRightById(u64),
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
SwapWindowLeft,
SwapWindowRight,
CenterColumn,
CenterWindow,
#[knuffel(skip)]
CenterWindowById(u64),
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
@@ -1222,18 +1263,39 @@ pub enum Action {
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveWorkspaceDown,
MoveWorkspaceUp,
SetWorkspaceName(#[knuffel(argument)] String),
#[knuffel(skip)]
SetWorkspaceNameByRef {
name: String,
reference: WorkspaceReference,
},
UnsetWorkspaceName,
#[knuffel(skip)]
UnsetWorkSpaceNameByRef(#[knuffel(argument)] WorkspaceReference),
FocusMonitorLeft,
FocusMonitorRight,
FocusMonitorDown,
FocusMonitorUp,
FocusMonitorPrevious,
FocusMonitorNext,
MoveWindowToMonitorLeft,
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
MoveWindowToMonitorPrevious,
MoveWindowToMonitorNext,
MoveColumnToMonitorLeft,
MoveColumnToMonitorRight,
MoveColumnToMonitorDown,
MoveColumnToMonitorUp,
MoveColumnToMonitorPrevious,
MoveColumnToMonitorNext,
SetWindowWidth(#[knuffel(argument, str)] SizeChange),
#[knuffel(skip)]
SetWindowWidthById {
id: u64,
change: SizeChange,
},
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
#[knuffel(skip)]
SetWindowHeightById {
@@ -1244,6 +1306,9 @@ pub enum Action {
#[knuffel(skip)]
ResetWindowHeightById(u64),
SwitchPresetColumnWidth,
SwitchPresetWindowWidth,
#[knuffel(skip)]
SwitchPresetWindowWidthById(u64),
SwitchPresetWindowHeight,
#[knuffel(skip)]
SwitchPresetWindowHeightById(u64),
@@ -1255,6 +1320,26 @@ pub enum Action {
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorUp,
MoveWorkspaceToMonitorPrevious,
MoveWorkspaceToMonitorNext,
ToggleWindowFloating,
#[knuffel(skip)]
ToggleWindowFloatingById(u64),
MoveWindowToFloating,
#[knuffel(skip)]
MoveWindowToFloatingById(u64),
MoveWindowToTiling,
#[knuffel(skip)]
MoveWindowToTilingById(u64),
FocusFloating,
FocusTiling,
SwitchFocusBetweenFloatingAndTiling,
#[knuffel(skip)]
MoveFloatingWindowById {
id: Option<u64>,
x: PositionChange,
y: PositionChange,
},
}
impl From<niri_ipc::Action> for Action {
@@ -1274,6 +1359,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow,
niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id),
niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id),
niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious,
niri_ipc::Action::FocusColumnLeft {} => Self::FocusColumnLeft,
niri_ipc::Action::FocusColumnRight {} => Self::FocusColumnRight,
niri_ipc::Action::FocusColumnFirst {} => Self::FocusColumnFirst,
@@ -1322,7 +1408,11 @@ impl From<niri_ipc::Action> for Action {
}
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight,
niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft,
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
niri_ipc::Action::FocusWorkspaceDown {} => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp {} => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { reference } => {
@@ -1349,18 +1439,44 @@ impl From<niri_ipc::Action> for Action {
}
niri_ipc::Action::MoveWorkspaceDown {} => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp {} => Self::MoveWorkspaceUp,
niri_ipc::Action::SetWorkspaceName {
name,
workspace: None,
} => Self::SetWorkspaceName(name),
niri_ipc::Action::SetWorkspaceName {
name,
workspace: Some(reference),
} => Self::SetWorkspaceNameByRef {
name,
reference: WorkspaceReference::from(reference),
},
niri_ipc::Action::UnsetWorkspaceName { reference: None } => Self::UnsetWorkspaceName,
niri_ipc::Action::UnsetWorkspaceName {
reference: Some(reference),
} => Self::UnsetWorkSpaceNameByRef(WorkspaceReference::from(reference)),
niri_ipc::Action::FocusMonitorLeft {} => Self::FocusMonitorLeft,
niri_ipc::Action::FocusMonitorRight {} => Self::FocusMonitorRight,
niri_ipc::Action::FocusMonitorDown {} => Self::FocusMonitorDown,
niri_ipc::Action::FocusMonitorUp {} => Self::FocusMonitorUp,
niri_ipc::Action::FocusMonitorPrevious {} => Self::FocusMonitorPrevious,
niri_ipc::Action::FocusMonitorNext {} => Self::FocusMonitorNext,
niri_ipc::Action::MoveWindowToMonitorLeft {} => Self::MoveWindowToMonitorLeft,
niri_ipc::Action::MoveWindowToMonitorRight {} => Self::MoveWindowToMonitorRight,
niri_ipc::Action::MoveWindowToMonitorDown {} => Self::MoveWindowToMonitorDown,
niri_ipc::Action::MoveWindowToMonitorUp {} => Self::MoveWindowToMonitorUp,
niri_ipc::Action::MoveWindowToMonitorPrevious {} => Self::MoveWindowToMonitorPrevious,
niri_ipc::Action::MoveWindowToMonitorNext {} => Self::MoveWindowToMonitorNext,
niri_ipc::Action::MoveColumnToMonitorLeft {} => Self::MoveColumnToMonitorLeft,
niri_ipc::Action::MoveColumnToMonitorRight {} => Self::MoveColumnToMonitorRight,
niri_ipc::Action::MoveColumnToMonitorDown {} => Self::MoveColumnToMonitorDown,
niri_ipc::Action::MoveColumnToMonitorUp {} => Self::MoveColumnToMonitorUp,
niri_ipc::Action::MoveColumnToMonitorPrevious {} => Self::MoveColumnToMonitorPrevious,
niri_ipc::Action::MoveColumnToMonitorNext {} => Self::MoveColumnToMonitorNext,
niri_ipc::Action::SetWindowWidth { id: None, change } => Self::SetWindowWidth(change),
niri_ipc::Action::SetWindowWidth {
id: Some(id),
change,
} => Self::SetWindowWidthById { id, change },
niri_ipc::Action::SetWindowHeight { id: None, change } => Self::SetWindowHeight(change),
niri_ipc::Action::SetWindowHeight {
id: Some(id),
@@ -1369,6 +1485,10 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight,
niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id),
niri_ipc::Action::SwitchPresetColumnWidth {} => Self::SwitchPresetColumnWidth,
niri_ipc::Action::SwitchPresetWindowWidth { id: None } => Self::SwitchPresetWindowWidth,
niri_ipc::Action::SwitchPresetWindowWidth { id: Some(id) } => {
Self::SwitchPresetWindowWidthById(id)
}
niri_ipc::Action::SwitchPresetWindowHeight { id: None } => {
Self::SwitchPresetWindowHeight
}
@@ -1383,9 +1503,33 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::MoveWorkspaceToMonitorRight {} => Self::MoveWorkspaceToMonitorRight,
niri_ipc::Action::MoveWorkspaceToMonitorDown {} => Self::MoveWorkspaceToMonitorDown,
niri_ipc::Action::MoveWorkspaceToMonitorUp {} => Self::MoveWorkspaceToMonitorUp,
niri_ipc::Action::MoveWorkspaceToMonitorPrevious {} => {
Self::MoveWorkspaceToMonitorPrevious
}
niri_ipc::Action::MoveWorkspaceToMonitorNext {} => Self::MoveWorkspaceToMonitorNext,
niri_ipc::Action::ToggleDebugTint {} => Self::ToggleDebugTint,
niri_ipc::Action::DebugToggleOpaqueRegions {} => Self::DebugToggleOpaqueRegions,
niri_ipc::Action::DebugToggleDamage {} => Self::DebugToggleDamage,
niri_ipc::Action::ToggleWindowFloating { id: None } => Self::ToggleWindowFloating,
niri_ipc::Action::ToggleWindowFloating { id: Some(id) } => {
Self::ToggleWindowFloatingById(id)
}
niri_ipc::Action::MoveWindowToFloating { id: None } => Self::MoveWindowToFloating,
niri_ipc::Action::MoveWindowToFloating { id: Some(id) } => {
Self::MoveWindowToFloatingById(id)
}
niri_ipc::Action::MoveWindowToTiling { id: None } => Self::MoveWindowToTiling,
niri_ipc::Action::MoveWindowToTiling { id: Some(id) } => {
Self::MoveWindowToTilingById(id)
}
niri_ipc::Action::FocusFloating {} => Self::FocusFloating,
niri_ipc::Action::FocusTiling {} => Self::FocusTiling,
niri_ipc::Action::SwitchFocusBetweenFloatingAndTiling {} => {
Self::SwitchFocusBetweenFloatingAndTiling
}
niri_ipc::Action::MoveFloatingWindow { id, x, y } => {
Self::MoveFloatingWindowById { id, x, y }
}
}
}
}
@@ -1525,9 +1669,13 @@ pub struct DebugConfig {
pub disable_cursor_plane: bool,
#[knuffel(child)]
pub disable_direct_scanout: bool,
#[knuffel(child)]
pub restrict_primary_scanout_to_matching_format: bool,
#[knuffel(child, unwrap(argument))]
pub render_drm_device: Option<PathBuf>,
#[knuffel(child)]
pub force_pipewire_invalid_modifier: bool,
#[knuffel(child)]
pub emulate_zero_presentation_time: bool,
#[knuffel(child)]
pub disable_resize_throttling: bool,
@@ -1537,6 +1685,8 @@ pub struct DebugConfig {
pub keep_laptop_panel_on_when_lid_is_closed: bool,
#[knuffel(child)]
pub disable_monitor_names: bool,
#[knuffel(child)]
pub strict_new_window_focus_policy: bool,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
@@ -1585,8 +1735,15 @@ impl Default for Config {
impl BorderRule {
pub fn merge_with(&mut self, other: &Self) {
self.off |= other.off;
self.on |= other.on;
if other.off {
self.off = true;
self.on = false;
}
if other.on {
self.off = false;
self.on = true;
}
if let Some(x) = other.width {
self.width = Some(x);
@@ -2820,7 +2977,17 @@ impl FromStr for Key {
}
}
let trigger = if key.eq_ignore_ascii_case("WheelScrollDown") {
let trigger = if key.eq_ignore_ascii_case("MouseLeft") {
Trigger::MouseLeft
} else if key.eq_ignore_ascii_case("MouseRight") {
Trigger::MouseRight
} else if key.eq_ignore_ascii_case("MouseMiddle") {
Trigger::MouseMiddle
} else if key.eq_ignore_ascii_case("MouseBack") {
Trigger::MouseBack
} else if key.eq_ignore_ascii_case("MouseForward") {
Trigger::MouseForward
} else if key.eq_ignore_ascii_case("WheelScrollDown") {
Trigger::WheelScrollDown
} else if key.eq_ignore_ascii_case("WheelScrollUp") {
Trigger::WheelScrollUp
@@ -2929,7 +3096,8 @@ pub fn set_miette_hook() -> Result<(), miette::InstallError> {
#[cfg(test)]
mod tests {
use k9::snapshot;
use insta::{assert_debug_snapshot, assert_snapshot};
use niri_ipc::PositionChange;
use pretty_assertions::assert_eq;
use super::*;
@@ -3117,6 +3285,10 @@ mod tests {
open-on-output "eDP-1"
open-maximized true
open-fullscreen false
open-floating false
open-focused true
default-window-height { fixed 500; }
default-floating-position x=100 y=-200 relative-to="bottom-left"
focus-ring {
off
@@ -3129,6 +3301,11 @@ mod tests {
}
}
layer-rule {
match namespace="^notifications$"
block-out-from "screencast"
}
binds {
Mod+T allow-when-locked=true { spawn "alacritty"; }
Mod+Q { close-window; }
@@ -3183,7 +3360,7 @@ mod tests {
left_handed: false,
disabled_on_external_mouse: true,
middle_emulation: false,
scroll_factor: FloatOrInt(0.9),
scroll_factor: Some(FloatOrInt(0.9)),
},
mouse: Mouse {
off: false,
@@ -3194,7 +3371,7 @@ mod tests {
scroll_button: Some(273),
left_handed: false,
middle_emulation: true,
scroll_factor: FloatOrInt(0.2),
scroll_factor: Some(FloatOrInt(0.2)),
},
trackpoint: Trackpoint {
off: true,
@@ -3308,6 +3485,7 @@ mod tests {
},
center_focused_column: CenterFocusedColumn::OnOverflow,
always_center_single_column: false,
empty_workspace_above_first: false,
},
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
@@ -3361,20 +3539,22 @@ mod tests {
]),
window_rules: vec![WindowRule {
matches: vec![Match {
app_id: Some(Regex::new(".*alacritty").unwrap()),
app_id: Some(RegexEq::from_str(".*alacritty").unwrap()),
title: None,
is_active: None,
is_focused: None,
is_active_in_column: None,
is_floating: None,
at_startup: None,
}],
excludes: vec![
Match {
app_id: None,
title: Some(Regex::new("~").unwrap()),
title: Some(RegexEq::from_str("~").unwrap()),
is_active: None,
is_focused: None,
is_active_in_column: None,
is_floating: None,
at_startup: None,
},
Match {
@@ -3383,12 +3563,21 @@ mod tests {
is_active: Some(true),
is_focused: Some(false),
is_active_in_column: None,
is_floating: None,
at_startup: None,
},
],
open_on_output: Some("eDP-1".to_owned()),
open_maximized: Some(true),
open_fullscreen: Some(false),
open_floating: Some(false),
open_focused: Some(true),
default_window_height: Some(DefaultPresetSize(Some(PresetSize::Fixed(500)))),
default_floating_position: Some(FoIPosition {
x: FloatOrInt(100.),
y: FloatOrInt(-200.),
relative_to: RelativeTo::BottomLeft,
}),
focus_ring: BorderRule {
off: true,
width: Some(FloatOrInt(3.)),
@@ -3401,6 +3590,17 @@ mod tests {
},
..Default::default()
}],
layer_rules: vec![
LayerRule {
matches: vec![layer_rule::Match {
namespace: Some(RegexEq::from_str("^notifications$").unwrap()),
at_startup: None,
}],
excludes: vec![],
opacity: None,
block_out_from: Some(BlockOutFrom::Screencast),
}
],
workspaces: vec![
Workspace {
name: WorkspaceName("workspace-1".to_string()),
@@ -3597,6 +3797,28 @@ mod tests {
assert!("10% ".parse::<SizeChange>().is_err());
}
#[test]
fn parse_position_change() {
assert_eq!(
"10".parse::<PositionChange>().unwrap(),
PositionChange::SetFixed(10.),
);
assert_eq!(
"+10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(10.),
);
assert_eq!(
"-10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(-10.),
);
assert!("10%".parse::<PositionChange>().is_err());
assert!("+10%".parse::<PositionChange>().is_err());
assert!("-10%".parse::<PositionChange>().is_err());
assert!("-".parse::<PositionChange>().is_err());
assert!("10% ".parse::<PositionChange>().is_err());
}
#[test]
fn parse_gradient_interpolation() {
assert_eq!(
@@ -3812,22 +4034,79 @@ mod tests {
)
})
.collect::<Vec<_>>();
snapshot!(
assert_debug_snapshot!(
names,
r#"
[
"Unknown A A | DP-3",
"A Unknown A | DP-3",
"A A Unknown | DP-3",
"A A A | DP-4",
"A A A | DP-5",
"A A B | DP-3",
"A B A | DP-3",
"B A A | DP-3",
"DP-1 | DP-1",
"DP-2 | DP-2",
]
"#
@r#"
[
"Unknown A A | DP-3",
"A Unknown A | DP-3",
"A A Unknown | DP-3",
"A A A | DP-4",
"A A A | DP-5",
"A A B | DP-3",
"A B A | DP-3",
"B A A | DP-3",
"DP-1 | DP-1",
"DP-2 | DP-2",
]
"#
);
}
#[test]
fn test_border_rule_on_off_merging() {
fn is_on(config: &str, rules: &[&str]) -> String {
let mut resolved = BorderRule {
off: false,
on: false,
width: None,
active_color: None,
inactive_color: None,
active_gradient: None,
inactive_gradient: None,
};
for rule in rules.iter().copied() {
let rule = BorderRule {
off: rule == "off" || rule == "off,on",
on: rule == "on" || rule == "off,on",
..Default::default()
};
resolved.merge_with(&rule);
}
let config = Border {
off: config == "off",
..Default::default()
};
if resolved.resolve_against(config).off {
"off"
} else {
"on"
}
.to_owned()
}
assert_snapshot!(is_on("off", &[]), @"off");
assert_snapshot!(is_on("off", &["off"]), @"off");
assert_snapshot!(is_on("off", &["on"]), @"on");
assert_snapshot!(is_on("off", &["off,on"]), @"on");
assert_snapshot!(is_on("on", &[]), @"on");
assert_snapshot!(is_on("on", &["off"]), @"off");
assert_snapshot!(is_on("on", &["on"]), @"on");
assert_snapshot!(is_on("on", &["off,on"]), @"on");
assert_snapshot!(is_on("off", &["off", "off"]), @"off");
assert_snapshot!(is_on("off", &["off", "on"]), @"on");
assert_snapshot!(is_on("off", &["on", "off"]), @"off");
assert_snapshot!(is_on("off", &["on", "on"]), @"on");
assert_snapshot!(is_on("on", &["off", "off"]), @"off");
assert_snapshot!(is_on("on", &["off", "on"]), @"on");
assert_snapshot!(is_on("on", &["on", "off"]), @"off");
assert_snapshot!(is_on("on", &["on", "on"]), @"on");
}
}
+23
View File
@@ -0,0 +1,23 @@
use std::str::FromStr;
use regex::Regex;
/// `Regex` that implements `PartialEq` by its string form.
#[derive(Debug, Clone)]
pub struct RegexEq(pub Regex);
impl PartialEq for RegexEq {
fn eq(&self, other: &Self) -> bool {
self.0.as_str() == other.0.as_str()
}
}
impl Eq for RegexEq {}
impl FromStr for RegexEq {
type Err = <Regex as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Regex::from_str(s).map(Self)
}
}
+1 -1
View File
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=0.1.10"
niri-ipc = "=25.1.0"
```
+224 -3
View File
@@ -24,7 +24,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=0.1.10"
//! niri-ipc = "=25.1.0"
//! ```
//!
//! ## Features
@@ -55,6 +55,8 @@ pub enum Request {
Workspaces,
/// Request information about open windows.
Windows,
/// Request information about layer-shell surfaces.
Layers,
/// Request information about the configured keyboard layouts.
KeyboardLayouts,
/// Request information about the focused output.
@@ -119,6 +121,8 @@ pub enum Response {
Workspaces(Vec<Workspace>),
/// Information about open windows.
Windows(Vec<Window>),
/// Information about layer-shell surfaces.
Layers(Vec<LayerSurface>),
/// Information about the keyboard layout.
KeyboardLayouts(KeyboardLayouts),
/// Information about the focused output.
@@ -200,6 +204,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus the previously focused window.
FocusWindowPrevious {},
/// Focus the column to the left.
FocusColumnLeft {},
/// Focus the column to the right.
@@ -284,8 +290,24 @@ pub enum Action {
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right
SwapWindowRight {},
/// Swap focused window with one to the left
SwapWindowLeft {},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
#[cfg_attr(
feature = "clap",
clap(about = "Center the focused window on the screen")
)]
CenterWindow {
/// Id of the window to center.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -332,6 +354,34 @@ pub enum Action {
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Set the name of the focused workspace")
)]
SetWorkspaceName {
/// New name for the workspace.
#[cfg_attr(feature = "clap", arg())]
name: String,
/// Reference (index or name) of the workspace to name.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
workspace: Option<WorkspaceReferenceArg>,
},
/// Unset the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Unset the name of the focused workspace")
)]
UnsetWorkspaceName {
/// Reference (index or name) of the workspace to unname.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg())]
reference: Option<WorkspaceReferenceArg>,
},
/// Focus the monitor to the left.
FocusMonitorLeft {},
/// Focus the monitor to the right.
@@ -340,6 +390,10 @@ pub enum Action {
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp {},
/// Focus the previous monitor.
FocusMonitorPrevious {},
/// Focus the next monitor.
FocusMonitorNext {},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
@@ -348,6 +402,10 @@ pub enum Action {
MoveWindowToMonitorDown {},
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp {},
/// Move the focused window to the previous monitor.
MoveWindowToMonitorPrevious {},
/// Move the focused window to the next monitor.
MoveWindowToMonitorNext {},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
@@ -356,6 +414,26 @@ pub enum Action {
MoveColumnToMonitorDown {},
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp {},
/// Move the focused column to the previous monitor.
MoveColumnToMonitorPrevious {},
/// Move the focused column to the next monitor.
MoveColumnToMonitorNext {},
/// Change the width of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Change the width of the focused window")
)]
SetWindowWidth {
/// Id of the window whose width to set.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the width.
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Change the height of a window.
#[cfg_attr(
feature = "clap",
@@ -369,7 +447,7 @@ pub enum Action {
id: Option<u64>,
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Reset the height of a window back to automatic.
@@ -386,6 +464,14 @@ pub enum Action {
},
/// Switch between preset column widths.
SwitchPresetColumnWidth {},
/// Switch between preset window widths.
SwitchPresetWindowWidth {
/// Id of the window whose width to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
@@ -399,7 +485,7 @@ pub enum Action {
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg())]
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Switch between keyboard layouts.
@@ -418,12 +504,69 @@ pub enum Action {
MoveWorkspaceToMonitorDown {},
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp {},
/// Move the focused workspace to the previous monitor.
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage {},
/// Move the focused window between the floating and the tiling layout.
ToggleWindowFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the floating layout.
MoveWindowToFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the tiling layout.
MoveWindowToTiling {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switches focus to the floating layout.
FocusFloating {},
/// Switches focus to the tiling layout.
FocusTiling {},
/// Toggles the focus between the floating and the tiling layout.
SwitchFocusBetweenFloatingAndTiling {},
/// Move a floating window on screen.
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
MoveFloatingWindow {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the X position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
x: PositionChange,
/// How to change the Y position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
y: PositionChange,
},
}
/// Change in window or column size.
@@ -440,6 +583,16 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Change in floating window position.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum PositionChange {
/// Set the position in logical pixels.
SetFixed(f64),
/// Add or subtract to the current position in logical pixels.
AdjustFixed(f64),
}
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -695,12 +848,21 @@ pub struct Window {
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
/// Process ID that created the Wayland connection for this window, if known.
///
/// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
/// change in the future.
pub pid: Option<i32>,
/// Id of the workspace this window is on, if any.
pub workspace_id: Option<u64>,
/// Whether this window is currently focused.
///
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
pub is_focused: bool,
/// Whether this window is currently floating.
///
/// If the window isn't floating then it is in the tiling layout.
pub is_floating: bool,
}
/// Output configuration change result.
@@ -762,6 +924,46 @@ pub struct KeyboardLayouts {
pub current_idx: u8,
}
/// A layer-shell layer.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum Layer {
/// The background layer.
Background,
/// The bottom layer.
Bottom,
/// The top layer.
Top,
/// The overlay layer.
Overlay,
}
/// Keyboard interactivity modes for a layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum LayerSurfaceKeyboardInteractivity {
/// Surface cannot receive keyboard focus.
None,
/// Surface receives keyboard focus whenever possible.
Exclusive,
/// Surface receives keyboard focus on demand, e.g. when clicked.
OnDemand,
}
/// A layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct LayerSurface {
/// Namespace provided by the layer-shell client.
pub namespace: String,
/// Name of the output the surface is on.
pub output: String,
/// Layer that the surface is on.
pub layer: Layer,
/// The surface's keyboard interactivity mode.
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -891,6 +1093,25 @@ impl FromStr for SizeChange {
}
}
impl FromStr for PositionChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
+3 -3
View File
@@ -10,9 +10,9 @@ repository.workspace = true
[dependencies]
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.10", path = ".." }
niri-config = { version = "0.1.10", path = "../niri-config" }
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" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+7 -16
View File
@@ -1,15 +1,13 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientAngle {
angle: f32,
@@ -17,7 +15,7 @@ pub struct GradientAngle {
}
impl GradientAngle {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
@@ -31,20 +29,13 @@ impl TestCase for GradientAngle {
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
let delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.angle += delta.as_secs_f32() * PI;
if self.angle >= PI * 2. {
@@ -59,16 +50,16 @@ impl TestCase for GradientAngle {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
+8 -17
View File
@@ -1,16 +1,14 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientArea {
progress: f32,
@@ -19,7 +17,7 @@ pub struct GradientArea {
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: FloatOrInt(1.),
@@ -43,20 +41,13 @@ impl TestCase for GradientArea {
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
let delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.progress += delta.as_secs_f32() * PI;
if self.progress >= PI * 2. {
@@ -74,8 +65,8 @@ impl TestCase for GradientArea {
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
@@ -83,7 +74,7 @@ impl TestCase for GradientArea {
));
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
let mut g_area = Rectangle::new(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update_render_elements(
@@ -108,7 +99,7 @@ impl TestCase for GradientArea {
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
Rectangle::from_size(rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklab {
gradient_format: GradientInterpolation,
}
impl GradientOklab {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklab {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklabAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklabAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -29,16 +29,16 @@ impl TestCase for GradientOklabAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklchAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchDecreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchDecreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchDecreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchIncreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchIncreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchIncreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchLonger {
gradient_format: GradientInterpolation,
}
impl GradientOklchLonger {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchLonger {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchShorter {
gradient_format: GradientInterpolation,
}
impl GradientOklchShorter {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,16 +31,16 @@ impl TestCase for GradientOklchShorter {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
+6 -6
View File
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgb {
gradient_format: GradientInterpolation,
}
impl GradientSrgb {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -31,16 +31,16 @@ impl TestCase for GradientSrgb {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -29,16 +29,16 @@ impl TestCase for GradientSrgbAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbLinear {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinear {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -31,16 +31,16 @@ impl TestCase for GradientSrgbLinear {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbLinearAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinearAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -29,16 +29,16 @@ impl TestCase for GradientSrgbLinearAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
+68 -32
View File
@@ -1,18 +1,18 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
use niri::layout::{LayoutElement as _, Options};
use niri::animation::Clock;
use niri::layout::scrolling::ColumnWidth;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::{Color, FloatOrInt, OutputName};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::utils::{Logical, Physical, Size};
use smithay::utils::{Physical, Size};
use super::TestCase;
use super::{Args, TestCase};
use crate::test_window::TestWindow;
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
@@ -20,13 +20,16 @@ type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
pub struct Layout {
output: Output,
windows: Vec<TestWindow>,
clock: Clock,
layout: niri::layout::Layout<TestWindow>,
start_time: Duration,
steps: HashMap<Duration, DynStepFn>,
}
impl Layout {
pub fn new(size: Size<i32, Logical>) -> Self {
pub fn new(args: Args) -> Self {
let Args { size, clock } = args;
let output = Output::new(
String::new(),
PhysicalProperties {
@@ -63,20 +66,23 @@ impl Layout {
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(options);
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
layout.add_output(output.clone());
let start_time = clock.now_unadjusted();
Self {
output,
windows: Vec::new(),
clock,
layout,
start_time: get_monotonic_time(),
start_time,
steps: HashMap::new(),
}
}
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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)));
@@ -91,8 +97,8 @@ impl Layout {
rv
}
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
pub fn open_multiple_quickly(args: Args) -> Self {
let mut rv = Self::new(args);
for delay in [100, 200, 300] {
rv.add_step(delay, move |l| {
@@ -105,8 +111,8 @@ impl Layout {
rv
}
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
pub fn open_multiple_quickly_big(args: Args) -> Self {
let mut rv = Self::new(args);
for delay in [100, 200, 300] {
rv.add_step(delay, move |l| {
@@ -119,8 +125,8 @@ impl Layout {
rv
}
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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)));
@@ -135,8 +141,8 @@ impl Layout {
rv
}
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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)));
@@ -153,10 +159,24 @@ impl Layout {
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false, None);
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,
None,
);
window.communicate();
self.layout.add_window(window.clone(), width, false);
self.layout.add_window(
window.clone(),
AddWindowTarget::Auto,
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
@@ -167,11 +187,24 @@ impl Layout {
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false, None);
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,
None,
);
window.communicate();
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
self.layout.add_window(
window.clone(),
AddWindowTarget::NextTo(right_of.id()),
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
@@ -201,22 +234,25 @@ impl TestCase for Layout {
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
fn advance_animations(&mut self, _current_time: Duration) {
let now_unadjusted = self.clock.now_unadjusted();
let run = self
.steps
.keys()
.copied()
.filter(|delay| self.start_time + *delay <= current_time)
.filter(|delay| self.start_time + *delay <= now_unadjusted)
.collect::<Vec<_>>();
for key in &run {
let f = self.steps.remove(key).unwrap();
for delay in &run {
let now = self.start_time + *delay;
self.clock.set_unadjusted(now);
self.layout.advance_animations();
let f = self.steps.remove(delay).unwrap();
f(self);
}
if !run.is_empty() {
current_time = get_monotonic_time();
}
self.layout.advance_animations(current_time);
self.clock.set_unadjusted(now_unadjusted);
self.layout.advance_animations();
}
fn render(
@@ -228,7 +264,7 @@ impl TestCase for Layout {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.render_elements(renderer, RenderTarget::Output, true)
.map(|elem| Box::new(elem) as _)
.collect()
}
+7 -1
View File
@@ -1,8 +1,9 @@
use std::time::Duration;
use niri::animation::Clock;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
use smithay::utils::{Logical, Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
@@ -21,6 +22,11 @@ pub mod layout;
pub mod tile;
pub mod window;
pub struct Args {
pub size: Size<i32, Logical>,
pub clock: Clock,
}
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
+35 -29
View File
@@ -6,9 +6,9 @@ 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::{Logical, Physical, Point, Rectangle, Scale, Size};
use smithay::utils::{Physical, Point, Rectangle, Scale, Size};
use super::TestCase;
use super::{Args, TestCase};
use crate::test_window::TestWindow;
pub struct Tile {
@@ -17,53 +17,46 @@ pub struct Tile {
}
impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
pub fn freeform(args: Args) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
pub fn fixed_size(args: Args) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::freeform(size);
pub fn freeform_open(args: Args) -> Self {
let mut rv = Self::freeform(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size(size);
pub fn fixed_size_open(args: Args) -> Self {
let mut rv = Self::fixed_size(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(size);
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn with_window(window: TestWindow) -> Self {
pub fn with_window(args: Args, window: TestWindow) -> Self {
let Args { size, clock } = args;
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
@@ -77,15 +70,28 @@ impl Tile {
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
let mut tile = niri::layout::tile::Tile::new(
window.clone(),
size.to_f64(),
1.,
clock,
Rc::new(options),
);
tile.request_tile_size(size.to_f64(), false, None);
window.communicate();
Self { window, tile }
}
}
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
let size = Size::from((width, height)).to_f64();
self.tile
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
.update_config(size, 1., self.tile.options().clone());
self.tile.request_tile_size(size, false, None);
self.window.communicate();
}
@@ -93,8 +99,8 @@ impl TestCase for Tile {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time);
fn advance_animations(&mut self, _current_time: Duration) {
self.tile.advance_animations();
}
fn render(
@@ -108,7 +114,7 @@ impl TestCase for Tile {
self.tile.update(
true,
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
);
self.tile
.render(
+8 -8
View File
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use smithay::utils::{Physical, Point, Scale, Size};
use super::TestCase;
use super::{Args, TestCase};
use crate::test_window::TestWindow;
pub struct Window {
@@ -12,24 +12,24 @@ pub struct Window {
}
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
pub fn freeform(args: Args) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(size, false, None);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
pub fn fixed_size(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(size, false, None);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
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(size, false, None);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
+7 -15
View File
@@ -2,15 +2,11 @@
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use cases::Args;
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
use gtk::{gdk, gio, glib};
use niri::animation::ANIMATION_SLOWDOWN;
use smithay::utils::{Logical, Size};
use smithay_view::SmithayView;
use tracing_subscriber::EnvFilter;
@@ -66,24 +62,23 @@ fn on_startup(_app: &adw::Application) {
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
struct S {
stack: gtk::Stack,
anim_adjustment: gtk::Adjustment,
}
impl S {
fn add<T: TestCase + 'static>(
&self,
make: impl Fn(Size<i32, Logical>) -> T + 'static,
title: &str,
) {
let view = SmithayView::new(make);
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
let view = SmithayView::new(make, &self.anim_adjustment);
self.stack.add_titled(&view, None, title);
}
}
let s = S {
stack: stack.clone(),
anim_adjustment: anim_adjustment.clone(),
};
s.add(Window::freeform, "Freeform Window");
@@ -137,9 +132,6 @@ fn build_ui(app: &adw::Application) {
let content_headerbar = adw::HeaderBar::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
anim_adjustment
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
anim_scale.set_hexpand(true);
+38 -9
View File
@@ -1,18 +1,20 @@
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use smithay::utils::{Logical, Size};
use smithay::utils::Size;
use crate::cases::TestCase;
use crate::cases::{Args, TestCase};
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use std::time::Duration;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::animation::Clock;
use niri::render_helpers::{resources, shaders};
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -21,7 +23,7 @@ mod imp {
use super::*;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
#[derive(Default)]
pub struct SmithayView {
@@ -30,6 +32,7 @@ mod imp {
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
pub clock: RefCell<Clock>,
}
#[glib::object_subclass]
@@ -125,16 +128,24 @@ mod imp {
let size = self.size.get();
let frame_clock = self.obj().frame_clock().unwrap();
let time = Duration::from_micros(frame_clock.frame_time() as u64);
self.clock.borrow_mut().set_unadjusted(time);
// Create the test case if missing.
let mut case = self.test_case.borrow_mut();
let case = case.get_or_insert_with(|| {
let make = self.make_test_case.get().unwrap();
make(Size::from(size))
let args = Args {
size: Size::from(size),
clock: self.clock.borrow().clone(),
};
make(args)
});
case.advance_animations(get_monotonic_time());
case.advance_animations(self.clock.borrow_mut().now());
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
@@ -233,14 +244,32 @@ glib::wrapper! {
impl SmithayView {
pub fn new<T: TestCase + 'static>(
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
make_test_case: impl Fn(Args) -> T + 'static,
anim_adjustment: &gtk::Adjustment,
) -> Self {
let obj: Self = glib::Object::builder().build();
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
let make = move |args| Box::new(make_test_case(args)) as Box<dyn TestCase>;
let make_test_case = Box::new(make) as _;
let _ = obj.imp().make_test_case.set(make_test_case);
anim_adjustment.connect_value_changed({
let obj = obj.downgrade();
move |adj| {
if let Some(obj) = obj.upgrade() {
let mut clock = obj.imp().clock.borrow_mut();
let instantly = adj.value() == 0.0;
let rate = if instantly {
1.0
} else {
1.0 / adj.value().max(0.001)
};
clock.set_rate(rate);
clock.set_complete_instantly(instantly);
}
}
});
obj
}
}
+8 -2
View File
@@ -188,7 +188,7 @@ impl LayoutElement for TestWindow {
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
}
@@ -220,6 +220,8 @@ impl LayoutElement for TestWindow {
fn set_active_in_column(&mut self, _active: bool) {}
fn set_floating(&mut self, _floating: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn configure_intent(&self) -> ConfigureIntent {
@@ -240,6 +242,10 @@ impl LayoutElement for TestWindow {
self.inner.borrow().requested_size
}
fn is_child_of(&self, _parent: &Self) -> bool {
false
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
@@ -259,7 +265,7 @@ impl LayoutElement for TestWindow {
fn cancel_interactive_resize(&mut self) {}
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn on_commit(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
+9 -7
View File
@@ -33,7 +33,6 @@ Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 OR BSL-1.0
@@ -41,18 +40,21 @@ SourceLicense: GPL-3.0-or-later
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
# BSD-2-Clause
# BSD-2-Clause OR Apache-2.0 OR MIT
# BSD-3-Clause
# BSD-3-Clause OR MIT OR Apache-2.0
# GPL-3.0-or-later
# ISC
# MIT
# MIT AND (MIT OR Apache-2.0)
# MIT OR Apache-2.0
# (MIT OR Apache-2.0) AND BSD-3-Clause
# (MIT OR Apache-2.0) AND Unicode-3.0
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unicode-3.0
# Unlicense OR MIT
# 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 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 (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: ((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.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
@@ -101,10 +103,6 @@ Opening a new window never causes existing windows to resize.
%prep
{{{ git_dir_setup_macro }}}
# Make the version log message look nicer: since we're building not from niri's git repository,
# the git version macro will show its fallback string.
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
%cargo_prep -N
# We're doing an online build.
@@ -113,6 +111,9 @@ sed -i 's/^offline = true$//' .cargo/config.toml
# Final step in leaving alone our debug settings.
sed -i 's/.*please-remove-me$//' .cargo/config.toml
# Set the commit string.
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
%build
%cargo_build
@@ -127,6 +128,7 @@ 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
+21 -6
View File
@@ -250,6 +250,15 @@ window-rule {
default-column-width {}
}
// Open the Firefox picture-in-picture player as floating by default.
window-rule {
// This app-id regular expression will work for both:
// - host Firefox (app-id is "firefox")
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
open-floating true
}
// Example: block out two password managers from screen capture.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
@@ -441,15 +450,17 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// Consume one window from the right into the focused column.
Mod+Comma { consume-window-into-column; }
// Expel one window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// There are also commands that consume or expel a single window to the side.
// The following binds move the focused window in and out of a column.
// If the window is alone, they will consume it into the nearby column to the side.
// If the window is already in a column, they will expel it out.
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
// Consume one window from the right to the bottom of the focused column.
Mod+Comma { consume-window-into-column; }
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
@@ -472,6 +483,10 @@ binds {
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// Move the focused window between the floating and the tiling layout.
Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
// 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.
+1
View File
@@ -1,4 +1,5 @@
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk;
org.freedesktop.impl.portal.Notification=gtk;
org.freedesktop.impl.portal.Secret=gnome-keyring;
+202
View File
@@ -0,0 +1,202 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use crate::utils::get_monotonic_time;
/// Shareable lazy clock that can change rate.
///
/// The clock will fetch the time once and then retain it until explicitly cleared with
/// [`Clock::clear`].
#[derive(Debug, Default, Clone)]
pub struct Clock {
inner: Rc<RefCell<AdjustableClock>>,
}
#[derive(Debug, Default)]
struct LazyClock {
time: Option<Duration>,
}
/// Clock that can adjust its rate.
#[derive(Debug)]
struct AdjustableClock {
inner: LazyClock,
current_time: Duration,
last_seen_time: Duration,
rate: f64,
complete_instantly: bool,
}
impl Clock {
/// Creates a new clock with the given time.
pub fn with_time(time: Duration) -> Self {
let clock = AdjustableClock::new(LazyClock::with_time(time));
Self {
inner: Rc::new(RefCell::new(clock)),
}
}
/// Returns the current time.
pub fn now(&self) -> Duration {
self.inner.borrow_mut().now()
}
/// Returns the underlying time not adjusted for rate change.
pub fn now_unadjusted(&self) -> Duration {
self.inner.borrow_mut().inner.now()
}
/// Sets the unadjusted clock time.
pub fn set_unadjusted(&mut self, time: Duration) {
self.inner.borrow_mut().inner.set(time);
}
/// Clears the stored time so it's re-fetched again next.
pub fn clear(&mut self) {
self.inner.borrow_mut().inner.clear();
}
/// Gets the clock rate.
pub fn rate(&self) -> f64 {
self.inner.borrow().rate()
}
/// Sets the clock rate.
pub fn set_rate(&mut self, rate: f64) {
self.inner.borrow_mut().set_rate(rate);
}
/// Returns whether animations should complete instantly.
pub fn should_complete_instantly(&self) -> bool {
self.inner.borrow().should_complete_instantly()
}
/// Sets whether animations should complete instantly.
pub fn set_complete_instantly(&mut self, value: bool) {
self.inner.borrow_mut().set_complete_instantly(value);
}
}
impl PartialEq for Clock {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.inner, &other.inner)
}
}
impl Eq for Clock {}
impl LazyClock {
pub fn with_time(time: Duration) -> Self {
Self { time: Some(time) }
}
pub fn clear(&mut self) {
self.time = None;
}
pub fn set(&mut self, time: Duration) {
self.time = Some(time);
}
pub fn now(&mut self) -> Duration {
*self.time.get_or_insert_with(get_monotonic_time)
}
}
impl AdjustableClock {
pub fn new(mut inner: LazyClock) -> Self {
let time = inner.now();
Self {
inner,
current_time: time,
last_seen_time: time,
rate: 1.,
complete_instantly: false,
}
}
pub fn rate(&self) -> f64 {
self.rate
}
pub fn set_rate(&mut self, rate: f64) {
self.rate = rate.clamp(0., 1000.);
}
pub fn should_complete_instantly(&self) -> bool {
self.complete_instantly
}
pub fn set_complete_instantly(&mut self, value: bool) {
self.complete_instantly = value;
}
pub fn now(&mut self) -> Duration {
let time = self.inner.now();
if self.last_seen_time == time {
return self.current_time;
}
if self.last_seen_time < time {
let delta = time - self.last_seen_time;
let delta = delta.mul_f64(self.rate);
self.current_time = self.current_time.saturating_add(delta);
} else {
let delta = self.last_seen_time - time;
let delta = delta.mul_f64(self.rate);
self.current_time = self.current_time.saturating_sub(delta);
}
self.last_seen_time = time;
self.current_time
}
}
impl Default for AdjustableClock {
fn default() -> Self {
Self::new(LazyClock::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frozen_clock() {
let mut clock = Clock::with_time(Duration::ZERO);
assert_eq!(clock.now(), Duration::ZERO);
clock.set_unadjusted(Duration::from_millis(100));
assert_eq!(clock.now(), Duration::from_millis(100));
clock.set_unadjusted(Duration::from_millis(200));
assert_eq!(clock.now(), Duration::from_millis(200));
}
#[test]
fn rate_change() {
let mut clock = Clock::with_time(Duration::ZERO);
clock.set_rate(0.5);
clock.set_unadjusted(Duration::from_millis(100));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(100));
assert_eq!(clock.now(), Duration::from_millis(50));
clock.set_unadjusted(Duration::from_millis(200));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(200));
assert_eq!(clock.now(), Duration::from_millis(100));
clock.set_unadjusted(Duration::from_millis(150));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(150));
assert_eq!(clock.now(), Duration::from_millis(75));
clock.set_rate(2.0);
clock.set_unadjusted(Duration::from_millis(250));
assert_eq!(clock.now_unadjusted(), Duration::from_millis(250));
assert_eq!(clock.now(), Duration::from_millis(275));
}
}
+53 -98
View File
@@ -2,14 +2,12 @@ use std::time::Duration;
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
mod clock;
pub use clock::Clock;
#[derive(Debug, Clone)]
pub struct Animation {
@@ -23,7 +21,7 @@ pub struct Animation {
/// Best effort; not always exactly precise.
clamped_duration: Duration,
start_time: Duration,
current_time: Duration,
clock: Clock,
kind: Kind,
}
@@ -48,11 +46,17 @@ pub enum Curve {
}
impl Animation {
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
pub fn new(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
) -> Self {
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / clock.rate().max(0.001);
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
rv.is_off = true;
return rv;
@@ -71,7 +75,6 @@ impl Animation {
}
let start_time = self.start_time;
let current_time = self.current_time;
match config.kind {
niri_config::AnimationKind::Spring(p) => {
@@ -83,10 +86,11 @@ impl Animation {
initial_velocity: self.initial_velocity,
params,
};
*self = Self::spring(spring);
*self = Self::spring(self.clock.clone(), spring);
}
niri_config::AnimationKind::Easing(p) => {
*self = Self::ease(
self.clock.clone(),
self.from,
self.to,
self.initial_velocity,
@@ -97,7 +101,6 @@ impl Animation {
}
self.start_time = start_time;
self.current_time = current_time;
}
/// Restarts the animation using the previous config.
@@ -106,11 +109,12 @@ impl Animation {
return self.clone();
}
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
match self.kind {
Kind::Easing { curve } => Self::ease(
self.clock.clone(),
from,
to,
initial_velocity,
@@ -124,23 +128,32 @@ impl Animation {
initial_velocity: self.initial_velocity,
params: spring.params,
};
Self::spring(spring)
Self::spring(self.clock.clone(), spring)
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let threshold = 0.001; // FIXME
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
Self::decelerate(
self.clock.clone(),
from,
initial_velocity,
deceleration_rate,
threshold,
)
}
}
}
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
pub fn ease(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
duration_ms: u64,
curve: Curve,
) -> Self {
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
@@ -152,19 +165,15 @@ impl Animation {
duration,
// Our current curves never overshoot.
clamped_duration: duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn spring(spring: Spring) -> Self {
pub fn spring(clock: Clock, spring: Spring) -> Self {
let _span = tracy_client::span!("Animation::spring");
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
let kind = Kind::Spring(spring);
@@ -176,22 +185,19 @@ impl Animation {
is_off: false,
duration,
clamped_duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn decelerate(
clock: Clock,
from: f64,
initial_velocity: f64,
deceleration_rate: f64,
threshold: f64,
) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration_s = if initial_velocity == 0. {
0.
} else {
@@ -214,77 +220,26 @@ impl Animation {
is_off: false,
duration,
clamped_duration: duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn set_current_time(&mut self, time: Duration) {
if self.duration.is_zero() {
self.current_time = time;
return;
}
let end_time = self.start_time + self.duration;
if end_time <= self.current_time {
return;
}
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
if slowdown <= f64::EPSILON {
// Zero slowdown will cause the animation to end right away.
self.current_time = end_time;
return;
}
// We can't change current_time (since the incoming time values are always real-time), so
// apply the slowdown by shifting the start time to compensate.
if self.current_time <= time {
let delta = time - self.current_time;
let max_delta = end_time - self.current_time;
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
if slowdown <= min_slowdown {
// Our slowdown value will cause the animation to end right away.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time -= adjusted_delta - delta;
} else {
self.start_time += delta - adjusted_delta;
}
} else {
let delta = self.current_time - time;
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
if slowdown <= min_slowdown {
// Current time was about to jump to before the animation had started; let's just
// cancel the animation in this case.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time += adjusted_delta - delta;
} else {
self.start_time -= delta - adjusted_delta;
}
}
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.duration
}
pub fn is_clamped_done(&self) -> bool {
self.current_time >= self.start_time + self.clamped_duration
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.clamped_duration
}
pub fn value(&self) -> f64 {
@@ -292,7 +247,7 @@ impl Animation {
return self.to;
}
let passed = self.current_time.saturating_sub(self.start_time);
let passed = self.clock.now().saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
+140
View File
@@ -0,0 +1,140 @@
//! Headless backend for tests.
//!
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
//! crucial parts like rendering.
use std::mem;
use std::sync::{Arc, Mutex};
use niri_config::OutputName;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::RenderElementStates;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::utils::Size;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Headless {
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Headless {
pub fn new() -> Self {
Self {
ipc_outputs: Default::default(),
}
}
pub fn init(&mut self, _niri: &mut Niri) {}
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
let connector = format!("headless-{n}");
let make = "niri".to_string();
let model = "headless".to_string();
let serial = n.to_string();
let output = Output::new(
connector.clone(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: make.clone(),
model: model.clone(),
},
);
let mode = Mode {
size: Size::from((i32::from(size.0), i32::from(size.1))),
refresh: 60_000,
};
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
output.user_data().insert_if_missing(|| OutputName {
connector,
make: Some(make),
model: Some(model),
serial: Some(serial),
});
let physical_properties = output.physical_properties();
self.ipc_outputs.lock().unwrap().insert(
OutputId::next(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
serial: None,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: size.0,
height: size.1,
refresh_rate: 60_000,
is_preferred: true,
}],
current_mode: Some(0),
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
},
);
niri.add_output(output, None, false);
}
pub fn seat_name(&self) -> String {
"headless".to_owned()
}
pub fn with_primary_renderer<T>(
&mut self,
_f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
None
}
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
let states = RenderElementStates::default();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
get_monotonic_time(),
Refresh::Unknown,
0,
wp_presentation_feedback::Kind::empty(),
);
let output_state = niri.output_state.get_mut(output).unwrap();
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
}
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
// FIXME: request redraw on unfinished animations remain
RenderResult::Submitted
}
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
unimplemented!()
}
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
}
impl Default for Headless {
fn default() -> Self {
Self::new()
}
}
+27 -7
View File
@@ -17,9 +17,13 @@ pub use tty::Tty;
pub mod winit;
pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
pub enum Backend {
Tty(Tty),
Winit(Winit),
Headless(Headless),
}
#[derive(PartialEq, Eq)]
@@ -54,6 +58,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.init(niri),
Backend::Winit(winit) => winit.init(niri),
Backend::Headless(headless) => headless.init(niri),
}
}
@@ -61,6 +66,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.seat_name(),
Backend::Winit(winit) => winit.seat_name(),
Backend::Headless(headless) => headless.seat_name(),
}
}
@@ -71,6 +77,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.with_primary_renderer(f),
Backend::Winit(winit) => winit.with_primary_renderer(f),
Backend::Headless(headless) => headless.with_primary_renderer(f),
}
}
@@ -83,6 +90,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output),
Backend::Headless(headless) => headless.render(niri, output),
}
}
@@ -90,6 +98,7 @@ impl Backend {
match self {
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
Backend::Headless(_) => CompositorMod::Super,
}
}
@@ -97,6 +106,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.change_vt(vt),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -104,6 +114,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.suspend(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -111,6 +122,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.toggle_debug_tint(),
Backend::Winit(winit) => winit.toggle_debug_tint(),
Backend::Headless(_) => (),
}
}
@@ -118,6 +130,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
}
}
@@ -125,6 +138,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -132,6 +146,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
Backend::Headless(headless) => headless.ipc_outputs(),
}
}
@@ -143,6 +158,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
Backend::Headless(_) => None,
}
}
@@ -150,6 +166,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -157,6 +174,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -164,13 +182,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
}
}
pub fn on_debug_config_changed(&mut self) {
match self {
Backend::Tty(tty) => tty.on_debug_config_changed(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -197,4 +209,12 @@ impl Backend {
panic!("backend is not Winit")
}
}
pub fn headless(&mut self) -> &mut Headless {
if let Self::Headless(v) = self {
v
} else {
panic!("backend is not Headless")
}
}
}
+238 -251
View File
@@ -18,9 +18,9 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
use smithay::backend::drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
};
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLDevice, EGLDisplay};
@@ -50,6 +50,7 @@ use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlob
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::presentation::Refresh;
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
@@ -63,7 +64,12 @@ use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
Fourcc::Xrgb8888,
Fourcc::Xbgr8888,
Fourcc::Argb8888,
Fourcc::Abgr8888,
];
pub struct Tty {
config: Rc<RefCell<Config>>,
@@ -117,7 +123,7 @@ pub struct OutputDevice {
render_node: DrmNode,
drm_scanner: DrmScanner,
surfaces: HashMap<crtc::Handle, Surface>,
output_ids: HashMap<crtc::Handle, OutputId>,
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
// SAFETY: drop after all the objects used with them are dropped.
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
@@ -128,6 +134,13 @@ pub struct OutputDevice {
active_leases: Vec<DrmLease>,
}
// A connected, but not necessarily enabled, crtc.
#[derive(Debug, Clone)]
pub struct CrtcInfo {
id: OutputId,
name: OutputName,
}
impl OutputDevice {
pub fn lease_request(
&self,
@@ -167,6 +180,35 @@ impl OutputDevice {
pub fn remove_lease(&mut self, lease_id: u32) {
self.active_leases.retain(|l| l.id() != lease_id);
}
pub fn known_crtc_name(
&self,
crtc: &crtc::Handle,
conn: &connector::Info,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
let conn_name = format_connector_name(conn);
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
}
let Some(info) = self.known_crtcs.get(crtc) else {
let conn_name = format_connector_name(conn);
error!("crtc for connector {conn_name} missing from known");
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
};
info.name.clone()
}
}
#[derive(Debug, Clone, Copy)]
@@ -183,7 +225,6 @@ struct Surface {
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
pending_gamma_change: Option<Option<Vec<u16>>>,
vrr_enabled: bool,
/// Tracy frame that goes from vblank to vblank.
vblank_frame: Option<tracy_client::Frame>,
/// Frame name for the VBlank frame.
@@ -404,8 +445,6 @@ impl Tty {
self.device_changed(node.dev_id(), niri);
// Apply pending gamma changes and restore our existing gamma.
//
// Also, restore our VRR.
let device = self.devices.get_mut(&node).unwrap();
for (crtc, surface) in device.surfaces.iter_mut() {
if let Some(ramp) = surface.pending_gamma_change.take() {
@@ -423,33 +462,6 @@ impl Tty {
warn!("error restoring gamma: {err:?}");
}
}
// Restore VRR.
let output = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == node && tty_state.crtc == *crtc
})
.cloned();
let Some(output) = output else {
error!("missing output for crtc: {crtc:?}");
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name.connector);
continue;
};
try_to_change_vrr(
&device.drm,
surface.connector,
*crtc,
surface,
output_state,
surface.vrr_enabled,
);
}
}
@@ -596,7 +608,7 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
output_ids: HashMap::new(),
known_crtcs: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
@@ -629,6 +641,7 @@ impl Tty {
}
};
let mut added = Vec::new();
let mut removed = Vec::new();
for event in scan_result {
match event {
@@ -637,16 +650,16 @@ impl Tty {
crtc: Some(crtc),
} => {
let connector_name = format_connector_name(&connector);
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name, false);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
debug!(
"new connector: {} \"{}\"",
&output_name.connector,
output_name.format_make_model_serial(),
&name.connector,
name.format_make_model_serial(),
);
// Assign an id to this crtc.
device.output_ids.insert(crtc, OutputId::next());
let id = OutputId::next();
added.push((crtc, CrtcInfo { id, name }));
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
@@ -667,11 +680,42 @@ impl Tty {
};
for crtc in removed {
if device.output_ids.remove(&crtc).is_none() {
if device.known_crtcs.remove(&crtc).is_none() {
error!("output ID missing for disconnected crtc: {crtc:?}");
}
}
for (crtc, mut info) in added {
// Make/model/serial can match exactly between different physical monitors. This doesn't
// happen often, but our Layout does not support such duplicates and will panic.
//
// As a workaround, search for duplicates, and unname the new connectors if one is
// found. Connector names are always unique.
let name = &mut info.name;
let formatted = name.format_make_model_serial_or_connector();
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
if info.name.matches(&formatted) {
let connector = mem::take(&mut name.connector);
warn!(
"new connector {connector} duplicates make/model/serial \
of existing connector {}, unnaming",
info.name.connector,
);
*name = OutputName {
connector,
make: None,
model: None,
serial: None,
};
break;
}
}
// Insert it right away so next added connector will check against this one too.
let device = self.devices.get_mut(&node).unwrap();
device.known_crtcs.insert(crtc, info);
}
// This will connect any new connectors if needed, and apply other changes, such as
// connecting back the internal laptop monitor once it becomes the only monitor left.
//
@@ -763,12 +807,8 @@ impl Tty {
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name.clone(),
self.config.borrow().debug.disable_monitor_names,
);
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
@@ -821,45 +861,6 @@ impl Tty {
Err(err) => debug!("error setting max bpc: {err:?}"),
}
// Try to enable VRR if requested.
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
if enabled != vrr {
warn!("failed {} VRR", word);
}
vrr_enabled = enabled;
}
Err(err) => {
warn!("error {} VRR: {err:?}", word);
}
}
} else {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
// So that we can try it again later.
vrr_enabled = true;
}
}
} else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
let mut gamma_props = GammaProps::new(&device.drm, crtc)
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
.ok();
@@ -878,6 +879,31 @@ impl Tty {
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
// Try to enable VRR if requested.
match surface.vrr_supported(connector.handle()) {
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.use_vrr(vrr) {
warn!("error {} VRR: {err:?}", word);
}
}
Ok(VrrSupport::NotSupported) => {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector does not support it");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let _ = surface.use_vrr(false);
}
Err(err) => {
warn!("error querying for VRR support: {err:?}");
}
}
// Create GBM allocator.
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
@@ -904,23 +930,6 @@ impl Tty {
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
let config = self.config.borrow();
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
// Cursor planes have bugs on some systems.
let cursor_plane_gbm = if config.debug.disable_cursor_plane {
None
} else {
Some(device.gbm.clone())
};
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
let egl_context = renderer.as_ref().egl_context();
let render_formats = egl_context.dmabuf_render_formats();
@@ -959,7 +968,7 @@ impl Tty {
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
None,
allocator.clone(),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
@@ -967,7 +976,7 @@ impl Tty {
// formats, even though we only ever render on the primary GPU.
render_formats.clone(),
device.drm.cursor_size(),
cursor_plane_gbm.clone(),
Some(device.gbm.clone()),
);
let mut compositor = match res {
@@ -985,21 +994,17 @@ impl Tty {
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
let mut planes = surface.planes().clone();
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
None,
allocator,
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
cursor_plane_gbm,
Some(device.gbm.clone()),
)
.context("error creating DRM compositor")?
}
@@ -1008,7 +1013,6 @@ impl Tty {
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
compositor.use_direct_scanout(!config.debug.disable_direct_scanout);
let mut dmabuf_feedback = None;
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
@@ -1037,6 +1041,8 @@ impl Tty {
}
}
let vrr_enabled = compositor.vrr_enabled();
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
@@ -1054,7 +1060,6 @@ impl Tty {
compositor,
dmabuf_feedback,
gamma_props,
vrr_enabled,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
@@ -1233,10 +1238,17 @@ impl Tty {
// Mark the last frame as submitted.
match surface.compositor.frame_submitted() {
Ok(Some((mut feedback, target_presentation_time))) => {
let refresh = output_state
.frame_clock
.refresh_interval()
.unwrap_or(Duration::ZERO);
let refresh = match output_state.frame_clock.refresh_interval() {
Some(refresh) => {
if output_state.frame_clock.vrr() {
Refresh::Variable(refresh)
} else {
Refresh::Fixed(refresh)
}
}
None => Refresh::Unknown,
};
// FIXME: ideally should be monotonically increasing for a surface.
let seq = meta.sequence as u64;
let mut flags = wp_presentation_feedback::Kind::Vsync
@@ -1386,9 +1398,35 @@ impl Tty {
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
let flags = {
let debug = &self.config.borrow().debug;
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
} else {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
};
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
if debug.enable_overlay_planes {
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_direct_scanout {
flags.remove(primary_scanout_flag);
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_cursor_plane {
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
}
flags
};
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
Ok(res) => {
let needs_sync = res.needs_sync()
|| self
@@ -1569,17 +1607,13 @@ impl Tty {
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
let mut ipc_outputs = HashMap::new();
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name.clone(),
self.config.borrow().debug.disable_monitor_names,
);
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1614,8 +1648,17 @@ impl Tty {
}
}
let vrr_supported = is_vrr_capable(&device.drm, connector.handle()) == Some(true);
let vrr_enabled = surface.map_or(false, |surface| surface.vrr_enabled);
let vrr_supported = surface
.map(|surface| {
matches!(
surface.compositor.vrr_supported(connector.handle()),
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
)
})
.unwrap_or_else(|| {
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
});
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
let logical = niri
.global_space
@@ -1626,6 +1669,12 @@ impl Tty {
})
.map(logical_output);
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
let id = id.unwrap_or_else(|| {
error!("crtc for connector {connector_name} missing from known");
OutputId::next()
});
let ipc_output = niri_ipc::Output {
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
@@ -1639,10 +1688,6 @@ impl Tty {
logical,
};
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
error!("output ID missing for crtc: {crtc:?}");
OutputId::next()
});
ipc_outputs.insert(id, ipc_output);
}
}
@@ -1700,14 +1745,17 @@ impl Tty {
for (&crtc, surface) in device.surfaces.iter_mut() {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
if tty_state.node == node && tty_state.crtc == crtc {
try_to_change_vrr(
&device.drm,
surface.connector,
crtc,
surface,
output_state,
enable_vrr,
);
let word = if enable_vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
self.refresh_ipc_outputs(niri);
return;
}
@@ -1773,8 +1821,11 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
let vrr_enabled = surface.compositor.vrr_enabled();
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
let is_on_demand_vrr = config.is_vrr_on_demand();
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1796,17 +1847,20 @@ impl Tty {
continue;
};
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
|| (!is_on_demand_vrr && change_always_vrr)
{
try_to_change_vrr(
&device.drm,
connector.handle(),
crtc,
surface,
output_state,
!surface.vrr_enabled,
);
let vrr = !vrr_enabled;
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
}
if change_mode {
@@ -1838,12 +1892,17 @@ impl Tty {
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output_state.frame_clock =
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
output_state.frame_clock = FrameClock::new(
Some(refresh_interval(mode)),
surface.compositor.vrr_enabled(),
);
niri.output_resized(&output);
}
}
let config = self.config.borrow();
let disable_monitor_names = config.debug.disable_monitor_names;
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
@@ -1859,16 +1918,9 @@ impl Tty {
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name,
self.config.borrow().debug.disable_monitor_names,
);
let config = self
.config
.borrow()
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let config = config
.outputs
.find(&output_name)
.cloned()
@@ -1897,24 +1949,12 @@ impl Tty {
self.refresh_ipc_outputs(niri);
}
pub fn on_debug_config_changed(&mut self) {
let config = self.config.borrow();
let debug = &config.debug;
let use_direct_scanout = !debug.disable_direct_scanout;
// FIXME: reload other flags if possible?
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
surface.compositor.use_direct_scanout(use_direct_scanout);
}
}
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
@@ -1931,13 +1971,7 @@ impl Tty {
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name,
self.config.borrow().debug.disable_monitor_names,
);
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
if output_name.matches(target) {
return Some(output_name);
}
@@ -2132,9 +2166,8 @@ fn surface_dmabuf_feedback(
let surface = compositor.surface();
let planes = surface.planes();
let plane_formats = surface
.plane_info()
.formats
let primary_plane_formats = surface.plane_info().formats.clone();
let primary_or_overlay_plane_formats = primary_plane_formats
.iter()
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
.copied()
@@ -2142,7 +2175,11 @@ fn surface_dmabuf_feedback(
// We limit the scan-out trache to formats we can also render from so that there is always a
// fallback render path available in case the supplied buffer can not be scanned out directly.
let mut scanout_formats = plane_formats
let mut primary_scanout_formats = primary_plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
@@ -2150,17 +2187,32 @@ fn surface_dmabuf_feedback(
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
// buffers produce a glitched scanout if the modifier is not Linear...
if primary_render_node != surface_render_node {
scanout_formats.retain(|f| f.modifier == Modifier::Linear);
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
}
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
trace!(
"primary scanout formats: {}, overlay adds: {}",
primary_scanout_formats.len(),
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
);
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
// increase the chance of scanning out a client even with our disabled-by-default overlay
// planes.
let scanout = builder
.clone()
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
scanout_formats,
primary_scanout_formats,
)
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
primary_or_overlay_scanout_formats,
)
.build()?;
@@ -2423,24 +2475,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
info.value_type().convert_value(value).as_boolean()
}
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
let (prop, info, _) =
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
device
.set_property(crtc, prop, value.into())
.context("error setting VRR_ENABLED property")?;
let value = get_drm_property(device, crtc, prop)
.context("VRR_ENABLED property missing after setting")?;
match info.value_type().convert_value(value) {
property::Value::UnsignedRange(value) => Ok(value == 1),
property::Value::Boolean(value) => Ok(value),
_ => bail!("wrong VRR_ENABLED property type"),
}
}
pub fn set_gamma_for_crtc(
device: &DrmDevice,
crtc: crtc::Handle,
@@ -2486,43 +2520,6 @@ pub fn set_gamma_for_crtc(
Ok(())
}
fn try_to_change_vrr(
device: &DrmDevice,
connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
enable_vrr: bool,
) {
let _span = tracy_client::span!("try_to_change_vrr");
if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name.connector
);
}
}
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
@@ -2535,17 +2532,7 @@ fn make_output_name(
device: &DrmDevice,
connector: connector::Handle,
connector_name: String,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
return OutputName {
connector: connector_name,
make: None,
model: None,
serial: None,
};
}
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
+2 -4
View File
@@ -3,7 +3,6 @@ use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
@@ -16,6 +15,7 @@ use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::Window;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
@@ -216,11 +216,9 @@ impl Winit {
self.backend.submit(Some(damage)).unwrap();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
let mode = output.current_mode().unwrap();
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
get_monotonic_time(),
refresh,
Refresh::Unknown,
0,
wp_presentation_feedback::Kind::empty(),
);
+2
View File
@@ -64,6 +64,8 @@ pub enum Msg {
Workspaces,
/// List open windows.
Windows,
/// List open layer-shell surfaces.
Layers,
/// Get the configured keyboard layouts.
KeyboardLayouts,
/// Print information about the focused output.
+5 -4
View File
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::message::Header;
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{dbus_interface, MessageHeader, Task};
use zbus::{interface, Task};
use super::Start;
@@ -20,11 +21,11 @@ pub struct ScreenSaver {
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
#[interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: MessageHeader<'_>,
#[zbus(header)] hdr: Header<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
@@ -33,7 +34,7 @@ impl ScreenSaver {
hdr.sender()
);
let Ok(Some(name)) = hdr.sender() else {
let Some(name) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
+5 -4
View File
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{SerializeDict, Type, Value};
use zbus::{dbus_interface, SignalContext};
use super::Start;
@@ -33,7 +34,7 @@ pub struct WindowProperties {
pub app_id: String,
}
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
#[interface(name = "org.gnome.Shell.Introspect")]
impl Introspect {
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
@@ -52,8 +53,8 @@ impl Introspect {
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
// needed for the event stream IPC anyway).
#[dbus_interface(signal)]
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
impl Introspect {
+2 -2
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use super::Start;
@@ -18,7 +18,7 @@ pub enum NiriToScreenshot {
ScreenshotResult(Option<PathBuf>),
}
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
#[interface(name = "org.gnome.Shell.Screenshot")]
impl Screenshot {
async fn screenshot(
&self,
+2 -4
View File
@@ -1,5 +1,5 @@
use zbus::blocking::Connection;
use zbus::Interface;
use zbus::object_server::Interface;
use crate::niri::State;
@@ -83,7 +83,7 @@ impl DBusServers {
dbus.conn_introspect = try_start(introspect);
#[cfg(feature = "xdp-gnome-screencast")]
if niri.pipewire.is_some() {
{
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -95,8 +95,6 @@ impl DBusServers {
.unwrap();
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}
}
+11 -10
View File
@@ -3,8 +3,9 @@ use std::sync::{Arc, Mutex};
use serde::Serialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
use zbus::{fdo, interface};
use super::Start;
use crate::backend::IpcOutputMap;
@@ -43,7 +44,7 @@ pub struct LogicalMonitor {
properties: HashMap<String, OwnedValue>,
}
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
&self,
@@ -156,8 +157,8 @@ impl DisplayConfig {
Ok((0, monitors, logical_monitors, properties))
}
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
@@ -212,16 +213,16 @@ fn format_diagonal(diagonal_inches: f64) -> String {
#[cfg(test)]
mod tests {
use k9::snapshot;
use insta::assert_snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
snapshot!(format_diagonal(12.11), "12.1″");
snapshot!(format_diagonal(13.28), "13.3″");
snapshot!(format_diagonal(15.6), "15.6″");
snapshot!(format_diagonal(23.2), "23″");
snapshot!(format_diagonal(24.8), "25″");
assert_snapshot!(format_diagonal(12.11), @"12.1″");
assert_snapshot!(format_diagonal(13.28), @"13.3″");
assert_snapshot!(format_diagonal(15.6), @"15.6″");
assert_snapshot!(format_diagonal(23.2), @"23″");
assert_snapshot!(format_diagonal(24.8), @"25″");
}
}
+16 -15
View File
@@ -5,8 +5,9 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::{InterfaceRef, SignalEmitter};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use zbus::{fdo, interface, ObjectServer};
use super::Start;
use crate::backend::IpcOutputMap;
@@ -94,14 +95,14 @@ pub enum ScreenCastToNiri {
session_id: usize,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
signal_ctx: SignalEmitter<'static>,
},
StopCast {
session_id: usize,
},
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
#[interface(name = "org.gnome.Mutter.ScreenCast")]
impl ScreenCast {
async fn create_session(
&self,
@@ -136,26 +137,26 @@ impl ScreenCast {
Ok(path)
}
#[dbus_interface(property)]
#[zbus(property)]
async fn version(&self) -> i32 {
4
}
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
impl Session {
async fn start(&self) {
debug!("start");
for (stream, iface) in &*self.streams.lock().unwrap() {
stream.start(self.id, iface.signal_context().clone());
stream.start(self.id, iface.signal_emitter().clone());
}
}
pub async fn stop(
&self,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
) {
debug!("stop");
@@ -175,7 +176,7 @@ impl Session {
let streams = mem::take(&mut *self.streams.lock().unwrap());
for (_, iface) in streams.iter() {
server
.remove::<Stream, _>(iface.signal_context().path())
.remove::<Stream, _>(iface.signal_emitter().path())
.await
.unwrap();
}
@@ -264,17 +265,17 @@ impl Session {
Ok(path)
}
#[dbus_interface(signal)]
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
#[zbus(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
#[zbus(property)]
async fn parameters(&self) -> StreamParameters {
match &self.target {
StreamTarget::Output(output) => {
@@ -361,7 +362,7 @@ impl Stream {
}
}
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
+8 -7
View File
@@ -1,9 +1,8 @@
use std::os::fd::{FromRawFd, IntoRawFd};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use smithay::reexports::wayland_server::DisplayHandle;
use zbus::dbus_interface;
use zbus::{fdo, interface, zvariant};
use super::Start;
use crate::niri::ClientState;
@@ -12,14 +11,14 @@ pub struct ServiceChannel {
display: DisplayHandle,
}
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
impl ServiceChannel {
async fn open_wayland_service_connection(
&mut self,
service_client_type: u32,
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
) -> fdo::Result<zvariant::OwnedFd> {
if service_client_type != 1 {
return Err(zbus::fdo::Error::InvalidArgs(
return Err(fdo::Error::InvalidArgs(
"Invalid service client type".to_owned(),
));
}
@@ -30,9 +29,11 @@ impl ServiceChannel {
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
restricted: false,
// FIXME: maybe you can get the PID from D-Bus somehow?
credentials_unknown: true,
});
self.display.insert_client(sock2, data).unwrap();
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
}
}
@@ -44,7 +45,7 @@ impl ServiceChannel {
impl Start for ServiceChannel {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::ConnectionBuilder::session()?
let conn = zbus::blocking::connection::Builder::session()?
.name("org.gnome.Mutter.ServiceChannel")?
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
.build()?;
+93 -30
View File
@@ -1,5 +1,6 @@
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::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
@@ -18,6 +19,8 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
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::utils::transaction::Transaction;
@@ -84,16 +87,23 @@ impl CompositorHandler for State {
if is_mapped {
// The toplevel got mapped.
let Unmapped { window, state } = entry.remove();
let Unmapped {
window,
state,
activation_token_data,
} = entry.remove();
window.on_commit();
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, is_full_width, output, workspace_name) =
let (rules, width, height, is_full_width, output, workspace_id) =
if let InitialConfigureState::Configured {
rules,
width,
height,
floating_width: _,
floating_height: _,
is_full_width,
output,
workspace_name,
@@ -104,15 +114,48 @@ impl CompositorHandler for State {
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
// Check that the workspace still exists.
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
let workspace_id = workspace_name
.as_deref()
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
.map(|(_, ws)| ws.id());
(rules, width, is_full_width, output, workspace_name)
(rules, width, height, is_full_width, output, workspace_id)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None, None)
(ResolvedWindowRules::empty(), None, None, false, None, None)
};
// The GTK about dialog sets min/max size after the initial configure but
// before mapping, so we need to compute open_floating at the last possible
// moment, that is here.
let is_floating = rules.compute_open_floating(toplevel);
// Figure out if we should activate the window.
let activate = rules.open_focused.map(|focus| {
if focus {
ActivateWindow::Yes
} else {
ActivateWindow::No
}
});
let activate = activate.unwrap_or_else(|| {
// Check the token timestamp again in case the window took a while between
// requesting activation and mapping.
let token = activation_token_data.filter(|token| {
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
});
if token.is_some() {
ActivateWindow::Yes
} else {
let config = self.niri.config.borrow();
if config.debug.strict_new_window_focus_policy {
ActivateWindow::No
} else {
ActivateWindow::Smart
}
}
});
let parent = toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
@@ -133,34 +176,34 @@ impl CompositorHandler for State {
let mapped = Mapped::new(window, rules, hook);
let window = mapped.window.clone();
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(workspace_name) = &workspace_name {
self.niri.layout.add_window_to_named_workspace(
workspace_name,
mapped,
width,
is_full_width,
)
let target = if let Some(p) = &parent {
// Open dialogs next to their parent window.
AddWindowTarget::NextTo(p)
} else if let Some(id) = workspace_id {
AddWindowTarget::Workspace(id)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
AddWindowTarget::Output(output)
} else {
self.niri.layout.add_window(mapped, width, is_full_width)
AddWindowTarget::Auto
};
let output = self.niri.layout.add_window(
mapped,
target,
width,
height,
is_full_width,
is_floating,
activate,
);
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
let new_focus = self.niri.layout.focus().map(|m| &m.window);
if new_focus == Some(&window) {
// We activated the newly opened window.
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
}
self.niri.queue_redraw(&output);
@@ -210,7 +253,7 @@ impl CompositorHandler for State {
// The toplevel got unmapped.
//
// Test client: wleird-unmap.
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
#[cfg(feature = "xdp-gnome-screencast")]
@@ -241,14 +284,21 @@ impl CompositorHandler for State {
return;
}
let serial = with_states(surface, |states| {
let (serial, buffer_delta) = with_states(surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
role.configure_serial
(role.configure_serial, buffer_delta)
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
@@ -257,8 +307,21 @@ impl CompositorHandler for State {
// The toplevel remains mapped.
self.niri.layout.update_window(&window, serial);
// Move the toplevel according to the attach offset.
if let Some(delta) = buffer_delta {
if delta.x != 0 || delta.y != 0 {
let (x, y) = delta.to_f64().into();
self.niri.layout.move_floating_window(
Some(&window),
PositionChange::AdjustFixed(x),
PositionChange::AdjustFixed(y),
false,
);
}
}
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
self.update_reactive_popups(&window);
self.niri.queue_redraw(&output);
return;
+18
View File
@@ -11,6 +11,7 @@ use smithay::wayland::shell::wlr_layer::{
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;
@@ -60,6 +61,7 @@ impl WlrLayerShellHandler for State {
layer.map(|layer| (o.clone(), map, layer))
}) {
map.unmap_layer(&layer);
self.niri.mapped_layer_surfaces.remove(&layer);
Some(output)
} else {
None
@@ -128,6 +130,21 @@ impl State {
if is_mapped {
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 rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let mapped = MappedLayer::new(layer.clone(), rules);
let prev = self
.niri
.mapped_layer_surfaces
.insert(layer.clone(), mapped);
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
@@ -151,6 +168,7 @@ impl State {
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
self.niri.mapped_layer_surfaces.remove(layer);
self.niri.unmapped_layer_surfaces.insert(surface.clone());
}
} else {
+48 -6
View File
@@ -62,8 +62,8 @@ use smithay::{
delegate_input_method_manager, 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_tablet_manager, delegate_text_input_manager, delegate_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
@@ -155,7 +155,7 @@ impl PointerConstraintsHandler for State {
location: Point<f64, Logical>,
) {
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
constraint.map_or(false, |c| c.is_active())
constraint.is_some_and(|c| c.is_active())
});
if !is_constraint_active {
@@ -306,7 +306,40 @@ impl ClientDndGrabHandler for State {
self.niri.queue_redraw_all();
}
fn dropped(&mut self, _seat: Seat<Self>) {
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
trace!("client dropped, target: {target:?}, validated: {validated}");
// 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;
}
}
}
if activate_output {
// Find the output from cursor coordinates.
//
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
// and if it comes from touch, then what the coordinates are. Need to pass more
// parameters from Smithay I guess.
//
// Assume that hidden pointer means touch DnD.
if !self.niri.pointer_hidden {
// 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.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
@@ -372,6 +405,10 @@ 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);
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
@@ -410,6 +447,7 @@ impl SecurityContextHandler for State {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
restricted: true,
credentials_unknown: false,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
@@ -637,10 +675,12 @@ impl XdgActivationHandler for State {
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
self.niri.activation_state.remove_token(&token);
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
unmapped.activation_token_data = Some(token_data);
}
}
self.niri.activation_state.remove_token(&token);
}
}
delegate_xdg_activation!(State);
@@ -662,3 +702,5 @@ delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
delegate_single_pixel_buffer!(State);
+120 -41
View File
@@ -42,7 +42,7 @@ 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::workspace::ColumnWidth;
use crate::layout::scrolling::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
@@ -209,8 +209,16 @@ impl XdgShellHandler for State {
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
let mut last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click gestures, so just allow it
// to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
@@ -281,6 +289,7 @@ impl XdgShellHandler for State {
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
trace!("ignoring popup grab because no root surface");
return;
};
@@ -289,30 +298,30 @@ impl XdgShellHandler for State {
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
trace!("ignoring popup grab because the session is locked");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
trace!("ignoring popup grab because the screenshot UI is open");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
// This is a grab for a regular window; check that there's no layer surface with a
// higher input priority.
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
trace!("ignoring toplevel popup grab because the overlay layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -325,33 +334,51 @@ impl XdgShellHandler for State {
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
trace!("ignoring toplevel popup grab because the top layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
trace!("ignoring toplevel popup grab because another window has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
trace!("ignoring popup grab because no output is active");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
let mut grab = match self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
{
Ok(grab) => grab,
Err(err) => {
trace!("ignoring popup grab: {err:?}");
return;
}
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let can_receive_keyboard_focus = self
.niri
.layout
.active_output()
.and_then(|output| {
layer_map_for_output(output)
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
})
.unwrap_or(true);
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
@@ -360,16 +387,22 @@ impl XdgShellHandler for State {
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
trace!("ignoring popup grab because of current grab mismatch");
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
if can_receive_keyboard_focus {
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
}
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
self.niri.popup_grab = Some(PopupGrabState {
root,
grab,
has_keyboard_grab: can_receive_keyboard_focus,
});
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
@@ -459,7 +492,7 @@ impl XdgShellHandler for State {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None, rules);
ws.configure_new_window(&unmapped.window, None, None, false, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -483,6 +516,9 @@ impl XdgShellHandler for State {
// 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();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
@@ -494,6 +530,9 @@ impl XdgShellHandler for State {
InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name,
@@ -548,12 +587,26 @@ impl XdgShellHandler for State {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
let is_floating = rules.compute_open_floating(&toplevel);
let configure_width = if is_floating {
*floating_width
} else if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width, rules);
let configure_height = if is_floating {
*floating_height
} else {
*height
};
ws.configure_new_window(
&unmapped.window,
configure_width,
configure_height,
is_floating,
rules,
);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -609,7 +662,7 @@ impl XdgShellHandler for State {
.start_close_animation_for_window(renderer, &window, blocker);
});
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window, transaction.clone());
@@ -641,6 +694,22 @@ impl XdgShellHandler for State {
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn parent_changed(&mut self, toplevel: ToplevelSurface) {
let Some(parent) = toplevel.parent() else {
return;
};
if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
let output = output.cloned();
let window = mapped.window.clone();
if self.niri.layout.descendants_added(&window) {
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
delegate_xdg_shell!(State);
@@ -752,7 +821,7 @@ impl State {
self.niri.is_at_startup,
);
let Unmapped { window, state } = unmapped;
let Unmapped { window, state, .. } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
@@ -814,7 +883,11 @@ impl State {
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let mut floating_width = None;
let mut height = None;
let mut floating_height = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
let is_floating = rules.compute_open_floating(toplevel);
// Tell the surface the preferred size and bounds for its likely output.
let ws = rules
@@ -836,14 +909,26 @@ impl State {
});
}
width = ws.resolve_default_width(rules.default_width);
width = ws.resolve_default_width(rules.default_width, false);
floating_width = ws.resolve_default_width(rules.default_width, true);
height = ws.resolve_default_height(rules.default_height, false);
floating_height = ws.resolve_default_height(rules.default_height, true);
let configure_width = if is_full_width {
let configure_width = if is_floating {
floating_width
} else if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width, &rules);
let configure_height = if is_floating { floating_height } else { height };
ws.configure_new_window(
window,
configure_width,
configure_height,
is_floating,
&rules,
);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
@@ -861,6 +946,9 @@ impl State {
*state = InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name().cloned()),
@@ -928,8 +1016,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@@ -939,19 +1027,10 @@ impl State {
}
}
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
let mut target = self.niri.layout.popup_target_rect(window);
target.loc -= get_popup_toplevel_coords(popup).to_f64();
self.position_popup_within_rect(popup, target);
@@ -971,7 +1050,7 @@ 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_loc_and_size((0, 0), output_geo.size);
let mut target = Rectangle::from_size(output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
@@ -1016,7 +1095,7 @@ impl State {
}
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
pub fn update_reactive_popups(&self, window: &Window) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(
@@ -1025,7 +1104,7 @@ impl State {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(xdg_popup, window, output);
self.unconstrain_window_popup(xdg_popup, window);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
+367 -46
View File
@@ -31,10 +31,12 @@ use smithay::input::SeatHandler;
use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER};
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use touch_move_grab::TouchMoveGrab;
use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::layout::scrolling::ScrollDirection;
use crate::niri::State;
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
@@ -82,11 +84,8 @@ impl State {
{
let _span = tracy_client::span!("process_input_event");
// A bit of a hack, but animation end runs some logic (i.e. workspace clean-up) and it
// doesn't always trigger due to damage, etc. So run it here right before it might prove
// important. Besides, animations affect the input, so it's best to have up-to-date values
// here.
self.niri.layout.advance_animations(get_monotonic_time());
// Make sure some logic like workspace clean-up has a chance to run before doing actions.
self.niri.advance_animations();
if self.niri.monitors_active {
// Notify the idle-notifier of activity.
@@ -120,7 +119,7 @@ impl State {
.niri
.exit_confirm_dialog
.as_ref()
.map_or(false, |d| d.is_open())
.is_some_and(|d| d.is_open())
&& should_hide_exit_confirm_dialog(&event);
use InputEvent::*;
@@ -522,7 +521,8 @@ impl State {
self.niri.debug_toggle_damage();
}
Action::Spawn(command) => {
spawn(command);
let (token, _) = self.niri.activation_state.create_external_token(None);
spawn(command, Some(token.clone()));
}
Action::DoScreenTransition(delay_ms) => {
self.backend.with_primary_renderer(|renderer| {
@@ -540,6 +540,10 @@ impl State {
}
}
Action::ConfirmScreenshot => {
if !self.niri.screenshot_ui.is_open() {
return;
}
self.backend.with_primary_renderer(|renderer| {
match self.niri.screenshot_ui.capture(renderer) {
Ok((size, pixels)) => {
@@ -560,6 +564,10 @@ impl State {
self.niri.queue_redraw_all();
}
Action::CancelScreenshot => {
if !self.niri.screenshot_ui.is_open() {
return;
}
self.niri.screenshot_ui.close();
self.niri
.cursor_manager
@@ -574,8 +582,8 @@ impl State {
self.open_screenshot_ui();
}
Action::ScreenshotWindow => {
let active = self.niri.layout.active_window();
if let Some((mapped, output)) = active {
let focus = self.niri.layout.focus_with_output();
if let Some((mapped, output)) = focus {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) {
warn!("error taking screenshot: {err:?}");
@@ -627,22 +635,12 @@ impl State {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
let active_output = self.niri.layout.active_output().cloned();
self.niri.layout.activate_window(&window);
let new_active = self.niri.layout.active_output().cloned();
#[allow(clippy::collapsible_if)]
if new_active != active_output {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&new_active.unwrap());
}
} else {
self.maybe_warp_cursor_to_focus();
}
// FIXME: granular
self.niri.queue_redraw_all();
self.focus_window(&window);
}
}
Action::FocusWindowPrevious => {
if let Some(window) = self.niri.previously_focused_window.clone() {
self.focus_window(&window);
}
}
Action::SwitchLayout(action) => {
@@ -771,36 +769,42 @@ impl State {
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRight => {
self.niri.layout.focus_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnFirst => {
self.niri.layout.focus_column_first();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLast => {
self.niri.layout.focus_column_last();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRightOrFirst => {
self.niri.layout.focus_column_right_or_first();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLeftOrLast => {
self.niri.layout.focus_column_left_or_last();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -817,6 +821,7 @@ impl State {
self.niri.layout.focus_up();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -834,6 +839,7 @@ impl State {
self.niri.layout.focus_down();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -851,6 +857,7 @@ impl State {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -868,6 +875,7 @@ impl State {
self.niri.layout.focus_right();
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -875,48 +883,56 @@ impl State {
Action::FocusWindowDown => {
self.niri.layout.focus_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUp => {
self.niri.layout.focus_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDownOrColumnLeft => {
self.niri.layout.focus_down_or_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDownOrColumnRight => {
self.niri.layout.focus_down_or_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUpOrColumnLeft => {
self.niri.layout.focus_up_or_left();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUpOrColumnRight => {
self.niri.layout.focus_up_or_right();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceUp => {
self.niri.layout.focus_window_or_workspace_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -973,7 +989,7 @@ impl State {
.niri
.layout
.active_output()
.map_or(false, |active| output.as_ref() == Some(active));
.is_some_and(|active| output.as_ref() == Some(active));
if let Some(output) = output {
self.niri
@@ -993,8 +1009,8 @@ impl State {
self.niri.layout.move_to_workspace(Some(&window), index);
// If we focused the target window.
let new_active_win = self.niri.layout.active_window();
if new_active_win.map_or(false, |(win, _)| win.window == window) {
let new_focus = self.niri.layout.focus();
if new_focus.is_some_and(|win| win.window == window) {
self.maybe_warp_cursor_to_focus();
}
}
@@ -1045,12 +1061,14 @@ impl State {
Action::FocusWorkspaceDown => {
self.niri.layout.switch_workspace_down();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspaceUp => {
self.niri.layout.switch_workspace_up();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -1079,6 +1097,7 @@ impl State {
}
self.maybe_warp_cursor_to_focus();
}
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
@@ -1086,6 +1105,8 @@ impl State {
}
Action::FocusWorkspacePrevious => {
self.niri.layout.switch_workspace_previous();
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
// FIXME: granular
self.niri.queue_redraw_all();
}
@@ -1099,6 +1120,18 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SetWorkspaceName(name) => {
self.niri.layout.set_workspace_name(name, None);
}
Action::SetWorkspaceNameByRef { name, reference } => {
self.niri.layout.set_workspace_name(name, Some(reference));
}
Action::UnsetWorkspaceName => {
self.niri.layout.unset_workspace_name(None);
}
Action::UnsetWorkSpaceNameByRef(reference) => {
self.niri.layout.unset_workspace_name(Some(reference));
}
Action::ConsumeWindowIntoColumn => {
self.niri.layout.consume_into_column();
// This does not cause immediate focus or window size change, so warping mouse to
@@ -1112,9 +1145,35 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwapWindowRight => {
self.niri
.layout
.swap_window_in_direction(ScrollDirection::Right);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwapWindowLeft => {
self.niri
.layout
.swap_window_in_direction(ScrollDirection::Left);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwitchPresetColumnWidth => {
self.niri.layout.toggle_width();
}
Action::SwitchPresetWindowWidth => {
self.niri.layout.toggle_window_width(None);
}
Action::SwitchPresetWindowWidthById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.toggle_window_width(Some(&window));
}
}
Action::SwitchPresetWindowHeight => {
self.niri.layout.toggle_window_height(None);
}
@@ -1130,6 +1189,20 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::CenterWindow => {
self.niri.layout.center_window(None);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::CenterWindowById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.center_window(Some(&window));
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MaximizeColumn => {
self.niri.layout.toggle_full_width();
}
@@ -1139,6 +1212,7 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorRight => {
@@ -1147,6 +1221,7 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorDown => {
@@ -1155,6 +1230,7 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorUp => {
@@ -1163,6 +1239,25 @@ impl State {
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::FocusMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
self.niri.layer_shell_on_demand_focus = None;
}
}
Action::MoveWindowToMonitorLeft => {
@@ -1201,6 +1296,24 @@ impl State {
}
}
}
Action::MoveWindowToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_to_output(None, &output, None);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_to_output(None, &output, None);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_column_to_output(&output);
@@ -1237,9 +1350,37 @@ impl State {
}
}
}
Action::MoveColumnToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::SetColumnWidth(change) => {
self.niri.layout.set_column_width(change);
}
Action::SetWindowWidth(change) => {
self.niri.layout.set_window_width(None, change);
}
Action::SetWindowWidthById { id, change } => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_width(Some(&window), change);
}
}
Action::SetWindowHeight(change) => {
self.niri.layout.set_window_height(None, change);
}
@@ -1297,6 +1438,100 @@ impl State {
}
}
}
Action::MoveWorkspaceToMonitorPrevious => {
if let Some(output) = self.niri.output_previous() {
self.niri.layout.move_workspace_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWorkspaceToMonitorNext => {
if let Some(output) = self.niri.output_next() {
self.niri.layout.move_workspace_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::ToggleWindowFloating => {
self.niri.layout.toggle_window_floating(None);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ToggleWindowFloatingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.toggle_window_floating(Some(&window));
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MoveWindowToFloating => {
self.niri.layout.set_window_floating(None, true);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToFloatingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_floating(Some(&window), true);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MoveWindowToTiling => {
self.niri.layout.set_window_floating(None, false);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToTilingById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.set_window_floating(Some(&window), false);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::FocusFloating => {
self.niri.layout.focus_floating();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusTiling => {
self.niri.layout.focus_tiling();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::SwitchFocusBetweenFloatingAndTiling => {
self.niri.layout.switch_focus_floating_tiling();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveFloatingWindowById { id, x, y } => {
let window = if let Some(id) = id {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if window.is_none() {
return;
}
window
} else {
None
};
self.niri
.layout
.move_floating_window(window.as_ref(), x, y, true);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
}
@@ -1550,11 +1785,43 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let button = event.button_code();
let button = event.button();
let button_code = event.button_code();
let button_state = event.state();
// Ignore release events for mouse clicks that triggered a bind.
if self.niri.suppressed_buttons.remove(&button_code) {
return;
}
if ButtonState::Pressed == button_state {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
if self.niri.mods_with_mouse_binds.contains(&modifiers) {
let comp_mod = self.backend.mod_key();
if let Some(bind) = match button {
Some(MouseButton::Left) => Some(Trigger::MouseLeft),
Some(MouseButton::Right) => Some(Trigger::MouseRight),
Some(MouseButton::Middle) => Some(Trigger::MouseMiddle),
Some(MouseButton::Back) => Some(Trigger::MouseBack),
Some(MouseButton::Forward) => Some(Trigger::MouseForward),
_ => None,
}
.and_then(|trigger| {
let config = self.niri.config.borrow();
let bindings = &config.binds;
find_configured_bind(bindings, comp_mod, trigger, mods)
}) {
self.niri.suppressed_buttons.insert(button_code);
self.handle_bind(bind.clone());
return;
};
}
// We received an event for the regular pointer, so show it now.
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
@@ -1563,8 +1830,7 @@ impl State {
let window = mapped.window.clone();
// Check if we need to start an interactive move.
if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1583,7 +1849,7 @@ impl State {
) {
let start_data = PointerGrabStartData {
focus: None,
button: event.button_code(),
button: button_code,
location,
};
let grab = MoveGrab::new(start_data, window.clone());
@@ -1595,8 +1861,7 @@ impl State {
}
}
// Check if we need to start an interactive resize.
else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1615,8 +1880,16 @@ impl State {
// FIXME: deduplicate with resize_request in xdg-shell somehow.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
let mut last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click
// gestures, so just allow it to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
@@ -1648,7 +1921,7 @@ impl State {
{
let start_data = PointerGrabStartData {
focus: None,
button: event.button_code(),
button: button_code,
location,
};
let grab = ResizeGrab::new(start_data, window.clone());
@@ -1672,8 +1945,7 @@ impl State {
self.niri.queue_redraw_all();
}
if event.button() == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
@@ -1683,7 +1955,7 @@ impl State {
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: event.button_code(),
button: button_code,
location,
};
let grab = SpatialMovementGrab::new(start_data, output);
@@ -1703,7 +1975,7 @@ impl State {
self.niri.focus_layer_surface_if_on_demand(layer_under);
}
if let Some(button) = event.button() {
if let Some(button) = button {
let pos = pointer.current_location();
if let Some((output, _)) = self.niri.output_under(pos) {
let output = output.clone();
@@ -1731,7 +2003,7 @@ impl State {
pointer.button(
self,
&ButtonEvent {
button,
button: button_code,
state: button_state,
serial,
time: event.time_msec(),
@@ -1743,6 +2015,12 @@ impl State {
fn on_pointer_axis<I: InputBackend>(&mut self, event: I::PointerAxisEvent) {
let source = event.source();
// We received an event for the regular pointer, so show it now. This is also needed for
// update_pointer_contents() below to return the real contents, necessary for the pointer
// axis event to reach the window.
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal);
let vertical_amount_v120 = event.amount_v120(Axis::Vertical);
@@ -1882,10 +2160,11 @@ impl State {
}
let scroll_factor = match source {
AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor.0,
AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor.0,
_ => 1.0,
AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor,
AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor,
_ => None,
};
let scroll_factor = scroll_factor.map(|x| x.0).unwrap_or(1.);
let horizontal_amount = horizontal_amount.unwrap_or_else(|| {
// Winit backend, discrete scrolling.
@@ -2352,12 +2631,40 @@ impl State {
return;
};
let serial = SERIAL_COUNTER.next_serial();
let under = self.niri.contents_under(touch_location);
if !handle.is_grabbed() {
if let Some(window) = under.window {
self.niri.layout.activate_window(&window);
// Check if we need to start an interactive move.
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
};
if mod_down {
let (output, pos_within_output) =
self.niri.output_under(touch_location).unwrap();
let output = output.clone();
if self.niri.layout.interactive_move_begin(
window.clone(),
&output,
pos_within_output,
) {
let start_data = TouchGrabStartData {
focus: None,
slot: evt.slot(),
location: touch_location,
};
let grab = TouchMoveGrab::new(start_data, window.clone());
handle.set_grab(self, grab, serial);
}
}
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = under.output {
@@ -2369,7 +2676,6 @@ impl State {
self.niri.focus_layer_surface_if_on_demand(under.layer);
};
let serial = SERIAL_COUNTER.next_serial();
handle.down(
self,
under.surface,
@@ -2962,6 +3268,20 @@ pub fn mods_with_binds(
rv
}
pub fn mods_with_mouse_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet<Modifiers> {
mods_with_binds(
comp_mod,
binds,
&[
Trigger::MouseLeft,
Trigger::MouseRight,
Trigger::MouseMiddle,
Trigger::MouseBack,
Trigger::MouseForward,
],
)
}
pub fn mods_with_wheel_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet<Modifiers> {
mods_with_binds(
comp_mod,
@@ -2991,6 +3311,7 @@ pub fn mods_with_finger_scroll_binds(comp_mod: CompositorMod, binds: &Binds) ->
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::Clock;
#[test]
fn bindings_suppress_keys() {
@@ -3009,7 +3330,7 @@ mod tests {
let comp_mod = CompositorMod::Super;
let mut suppressed_keys = HashSet::new();
let screenshot_ui = ScreenshotUi::new(Default::default());
let screenshot_ui = ScreenshotUi::new(Clock::default(), Default::default());
let disable_power_key_handling = false;
// The key_code we pick is arbitrary, the only thing
+12 -2
View File
@@ -123,8 +123,18 @@ impl PointerGrab<State> for MoveGrab {
}
}
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
} else {
0x110
};
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
data.niri.layout.toggle_window_floating(Some(&self.window));
}
if !handle.current_pressed().contains(&self.start_data.button) {
// The button that initiated the grab was released.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
+78
View File
@@ -1,3 +1,6 @@
use std::iter::Peekable;
use std::slice;
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
@@ -23,6 +26,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
},
Msg::Workspaces => Request::Workspaces,
Msg::Windows => Request::Windows,
Msg::Layers => Request::Layers,
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
@@ -168,6 +172,69 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!();
}
}
Msg::Layers => {
let Response::Layers(mut layers) = response else {
bail!("unexpected response: expected Layers, got {response:?}");
};
if json {
let layers = serde_json::to_string(&layers).context("error formatting response")?;
println!("{layers}");
return Ok(());
}
layers.sort_by(|a, b| {
Ord::cmp(&a.output, &b.output)
.then_with(|| Ord::cmp(&a.layer, &b.layer))
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
});
let mut iter = layers.iter().peekable();
let print = |surface: &niri_ipc::LayerSurface| {
println!(" Surface:");
println!(" Namespace: \"{}\"", &surface.namespace);
let interactivity = match surface.keyboard_interactivity {
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
};
println!(" Keyboard interactivity: {interactivity}");
};
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
output: &str,
layer| {
let mut empty = true;
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
empty = false;
println!();
print(surface);
}
if empty {
println!(" (empty)\n");
} else {
println!();
}
};
while let Some(surface) = iter.peek() {
let output = &surface.output;
println!("Output \"{output}\":");
print!(" Background layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Background);
print!(" Bottom layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
print!(" Top layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Top);
print!(" Overlay layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
}
}
Msg::FocusedOutput => {
let Response::FocusedOutput(output) = response else {
bail!("unexpected response: expected FocusedOutput, got {response:?}");
@@ -449,6 +516,17 @@ fn print_window(window: &Window) {
println!(" App ID: (unset)");
}
println!(
" Is floating: {}",
if window.is_floating { "yes" } else { "no" }
);
if let Some(pid) = window.pid {
println!(" PID: {pid}");
} else {
println!(" PID: (unknown)");
}
if let Some(workspace_id) = window.workspace_id {
println!(" Workspace ID: {workspace_id}");
} else {
+52 -3
View File
@@ -16,9 +16,11 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu
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::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use crate::backend::IpcOutputMap;
use crate::layout::workspace::WorkspaceId;
@@ -254,6 +256,47 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let windows = state.windows.windows.values().cloned().collect();
Response::Windows(windows)
}
Request::Layers => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let mut layers = Vec::new();
for output in state.niri.global_space.outputs() {
let name = output.name();
for surface in layer_map_for_output(output).layers() {
let layer = match surface.layer() {
Layer::Background => niri_ipc::Layer::Background,
Layer::Bottom => niri_ipc::Layer::Bottom,
Layer::Top => niri_ipc::Layer::Top,
Layer::Overlay => niri_ipc::Layer::Overlay,
};
let keyboard_interactivity =
match surface.cached_state().keyboard_interactivity {
KeyboardInteractivity::None => {
niri_ipc::LayerSurfaceKeyboardInteractivity::None
}
KeyboardInteractivity::Exclusive => {
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
}
KeyboardInteractivity::OnDemand => {
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
}
};
layers.push(niri_ipc::LayerSurface {
namespace: surface.namespace().to_owned(),
output: name.clone(),
layer,
keyboard_interactivity,
});
}
}
let _ = tx.send_blocking(layers);
});
let result = rx.recv().await;
let layers = result.map_err(|_| String::from("error getting layers info"))?;
Response::Layers(layers)
}
Request::KeyboardLayouts => {
let state = ctx.event_stream_state.borrow();
let layout = state.keyboard_layouts.keyboard_layouts.clone();
@@ -271,6 +314,9 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
// Make sure some logic like workspace clean-up has a chance to run before doing
// actions.
state.niri.advance_animations();
state.do_action(action, false);
let _ = tx.send_blocking(());
});
@@ -363,8 +409,10 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
pid: mapped.credentials().map(|c| c.pid),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
is_floating: mapped.is_floating(),
})
}
@@ -475,7 +523,7 @@ impl State {
}
// Check if this workspace became active.
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
if is_active && !ipc_ws.is_active {
events.push(Event::WorkspaceActivated { id, focused: false });
}
@@ -498,7 +546,7 @@ impl State {
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name().cloned(),
output: mon.map(|mon| mon.output_name().clone()),
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
is_focused: Some(id) == focused_ws_id,
active_window_id: ws.active_window().map(|win| win.id().get()),
}
@@ -545,7 +593,8 @@ impl State {
};
let workspace_id = ws_id.map(|id| id.get());
let mut changed = ipc_win.workspace_id != workspace_id;
let mut changed =
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
changed |= with_toplevel_role(mapped.toplevel(), |role| {
ipc_win.title != role.title || ipc_win.app_id != role.app_id
+122
View File
@@ -0,0 +1,122 @@
use std::cell::RefCell;
use niri_config::layer_rule::LayerRule;
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 super::ResolvedLayerRules;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
#[derive(Debug)]
pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,
/// Up-to-date rules.
rules: ResolvedLayerRules,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
}
niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
}
}
impl MappedLayer {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
Self {
surface,
rules,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
}
}
pub fn surface(&self) -> &LayerSurface {
&self.surface
}
pub fn rules(&self) -> &ResolvedLayerRules {
&self.rules
}
/// Recomputes the resolved layer rules and returns whether they changed.
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
if new_rules == self.rules {
return false;
}
self.rules = new_rules;
true
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
geometry: Rectangle<i32, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
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 mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(geometry.size.to_f64());
let elem = SolidColorRenderElement::from_buffer(
&buffer,
geometry.loc,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = geometry.loc;
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;
rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
));
}
rv.normal = render_elements_from_surface_tree(
renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
}
rv
}
}
+69
View File
@@ -0,0 +1,69 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::BlockOutFrom;
use smithay::desktop::LayerSurface;
pub mod mapped;
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.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
}
impl ResolvedLayerRules {
pub const fn empty() -> Self {
Self {
opacity: None,
block_out_from: None,
}
}
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
let _span = tracy_client::span!("ResolvedLayerRules::compute");
let mut resolved = ResolvedLayerRules::empty();
for rule in rules {
let matches = |m: &Match| {
if let Some(at_startup) = m.at_startup {
if at_startup != is_at_startup {
return false;
}
}
surface_matches(surface, m)
};
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}
if rule.excludes.iter().any(matches) {
continue;
}
if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
}
resolved
}
}
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
if let Some(namespace_re) = &m.namespace {
if !namespace_re.0.is_match(surface.namespace()) {
return false;
}
}
true
}
+3 -5
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -138,16 +137,15 @@ impl ClosingWindow {
})
}
pub fn advance_animations(&mut self, current_time: Duration) {
pub fn advance_animations(&mut self) {
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let mut anim = anim.restarted(0., 1., 0.);
anim.set_current_time(current_time);
let anim = anim.restarted(0., 1., 0.);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(anim) => anim.set_current_time(current_time),
AnimationState::Animating(_anim) => (),
}
}
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -94,7 +94,7 @@ impl FocusRing {
in_: GradientInterpolation::default(),
});
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
@@ -178,12 +178,12 @@ impl FocusRing {
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
Rectangle::new(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
@@ -196,15 +196,12 @@ impl FocusRing {
self.borders[0].update(
self.sizes[0],
Rectangle::from_loc_and_size(
gradient_area.loc - self.locations[0],
gradient_area.size,
),
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
+2090 -859
View File
File diff suppressed because it is too large Load Diff
+284 -271
View File
@@ -7,15 +7,15 @@ use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::scrolling::{Column, ColumnWidth, ScrollDirection};
use super::tile::Tile;
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use super::{ActivateWindow, LayoutElement, Options};
use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
@@ -45,6 +45,8 @@ pub struct Monitor<W: LayoutElement> {
pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub(super) workspace_switch: Option<WorkspaceSwitch>,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
pub(super) options: Rc<Options>,
}
@@ -66,6 +68,23 @@ pub struct WorkspaceSwitchGesture {
is_touchpad: bool,
}
/// Where to put a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
/// No particular preference.
#[default]
Auto,
/// On this workspace.
Workspace {
/// Id of the target workspace.
id: WorkspaceId,
/// Override where the window will open as a new column.
column_idx: Option<usize>,
},
/// Next to this existing window.
NextTo(&'a W::Id),
}
pub type MonitorRenderElement<R> =
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
@@ -84,6 +103,20 @@ impl WorkspaceSwitch {
}
}
pub fn offset(&mut self, delta: isize) {
match self {
WorkspaceSwitch::Animation(anim) => anim.offset(delta as f64),
WorkspaceSwitch::Gesture(gesture) => {
if delta >= 0 {
gesture.center_idx += delta as usize;
} else {
gesture.center_idx -= (-delta) as usize;
}
gesture.current_idx += delta as f64;
}
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
@@ -94,7 +127,12 @@ impl WorkspaceSwitch {
}
impl<W: LayoutElement> Monitor<W> {
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
pub fn new(
output: Output,
workspaces: Vec<Workspace<W>>,
clock: Clock,
options: Rc<Options>,
) -> Self {
Self {
output_name: output.name(),
output,
@@ -102,6 +140,7 @@ impl<W: LayoutElement> Monitor<W> {
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
clock,
options,
}
}
@@ -126,7 +165,7 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.iter().find(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
})
}
@@ -134,7 +173,7 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.iter().position(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
})
}
@@ -150,6 +189,29 @@ impl<W: LayoutElement> Monitor<W> {
self.windows().any(|win| win.id() == window)
}
pub fn add_workspace_top(&mut self) {
let ws = Workspace::new(
self.output.clone(),
self.clock.clone(),
self.options.clone(),
);
self.workspaces.insert(0, ws);
self.active_workspace_idx += 1;
if let Some(switch) = &mut self.workspace_switch {
switch.offset(1);
}
}
pub fn add_workspace_bottom(&mut self) {
let ws = Workspace::new(
self.output.clone(),
self.clock.clone(),
self.options.clone(),
);
self.workspaces.push(ws);
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
@@ -167,6 +229,7 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
current_idx,
idx as f64,
0.,
@@ -176,65 +239,34 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_window(
&mut self,
workspace_idx: usize,
window: W,
activate: bool,
target: MonitorAddWindowTarget<W>,
activate: ActivateWindow,
width: ColumnWidth,
is_full_width: bool,
is_floating: bool,
) {
// Currently, everything a workspace sets on a Tile is the same across all workspaces of a
// monitor. So we can use any workspace, not necessarily the exact target workspace.
let tile = self.workspaces[0].make_tile(window);
self.add_tile(tile, target, activate, width, is_full_width, is_floating);
}
pub fn add_column(&mut self, mut workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window(None, window, activate, width, is_full_width);
workspace.add_column(column, activate);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
self.add_workspace_bottom();
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace_idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(right_of))
.unwrap();
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window_right_of(right_of, window, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
// Since we're adding window right of something, the workspace isn't empty, and therefore
// cannot be the last one, so we never need to insert a new empty workspace.
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_column(None, column, activate, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
if self.options.empty_workspace_above_first && workspace_idx == 0 {
self.add_workspace_top();
workspace_idx += 1;
}
if activate {
@@ -244,27 +276,54 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_tile(
&mut self,
workspace_idx: usize,
column_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
target: MonitorAddWindowTarget<W>,
activate: ActivateWindow,
width: ColumnWidth,
is_full_width: bool,
is_floating: bool,
) {
let (mut workspace_idx, target) = match target {
MonitorAddWindowTarget::Auto => {
(self.active_workspace_idx, WorkspaceAddWindowTarget::Auto)
}
MonitorAddWindowTarget::Workspace { id, column_idx } => {
let idx = self.workspaces.iter().position(|ws| ws.id() == id).unwrap();
let target = if let Some(column_idx) = column_idx {
WorkspaceAddWindowTarget::NewColumnAt(column_idx)
} else {
WorkspaceAddWindowTarget::Auto
};
(idx, target)
}
MonitorAddWindowTarget::NextTo(win_id) => {
let idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(win_id))
.unwrap();
(idx, WorkspaceAddWindowTarget::NextTo(win_id))
}
};
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
workspace.add_tile(tile, target, activate, width, is_full_width, is_floating);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
self.add_workspace_bottom();
}
if activate {
if self.options.empty_workspace_above_first && workspace_idx == 0 {
self.add_workspace_top();
workspace_idx += 1;
}
if activate.map_smart(|| false) {
self.activate_workspace(workspace_idx);
}
}
@@ -295,40 +354,49 @@ impl<W: LayoutElement> Monitor<W> {
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
for idx in (0..self.workspaces.len() - 1).rev() {
let range_start = if self.options.empty_workspace_above_first {
1
} else {
0
};
for idx in (range_start..self.workspaces.len() - 1).rev() {
if self.active_workspace_idx == idx {
continue;
}
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
if !self.workspaces[idx].has_windows_or_name() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
}
}
}
}
pub fn unname_workspace(&mut self, workspace_name: &str) -> bool {
for ws in &mut self.workspaces {
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
{
ws.unname();
return true;
}
// Special case handling when empty_workspace_above_first is set and all workspaces
// are empty.
if self.options.empty_workspace_above_first && self.workspaces.len() == 2 {
assert!(!self.workspaces[0].has_windows_or_name());
assert!(!self.workspaces[1].has_windows_or_name());
self.workspaces.remove(1);
self.active_workspace_idx = 0;
}
false
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
pub fn unname_workspace(&mut self, id: WorkspaceId) -> bool {
let Some(ws) = self.workspaces.iter_mut().find(|ws| ws.id() == id) else {
return false;
};
ws.unname();
true
}
pub fn move_right(&mut self) {
self.active_workspace().move_right();
pub fn move_left(&mut self) -> bool {
self.active_workspace().move_left()
}
pub fn move_right(&mut self) -> bool {
self.active_workspace().move_right()
}
pub fn move_column_to_first(&mut self) {
@@ -348,40 +416,23 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_down_or_to_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let column = &mut workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
if !self.active_workspace().move_down() {
self.move_to_workspace_down();
} else {
workspace.move_down();
}
}
pub fn move_up_or_to_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
if !self.active_workspace().move_up() {
self.move_to_workspace_up();
} else {
workspace.move_up();
}
}
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
pub fn focus_left(&mut self) -> bool {
self.active_workspace().focus_left()
}
pub fn focus_right(&mut self) {
self.active_workspace().focus_right();
pub fn focus_right(&mut self) -> bool {
self.active_workspace().focus_right()
}
pub fn focus_column_first(&mut self) {
@@ -400,98 +451,39 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().focus_column_left_or_last();
}
pub fn focus_down(&mut self) {
self.active_workspace().focus_down();
pub fn focus_down(&mut self) -> bool {
self.active_workspace().focus_down()
}
pub fn focus_up(&mut self) {
self.active_workspace().focus_up();
pub fn focus_up(&mut self) -> bool {
self.active_workspace().focus_up()
}
pub fn focus_down_or_left(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_down();
}
}
self.active_workspace().focus_down_or_left();
}
pub fn focus_down_or_right(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_right();
} else {
workspace.focus_down();
}
}
self.active_workspace().focus_down_or_right();
}
pub fn focus_up_or_left(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_up();
}
}
self.active_workspace().focus_up_or_left();
}
pub fn focus_up_or_right(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_right();
} else {
workspace.focus_up();
}
}
self.active_workspace().focus_up_or_right();
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
if !self.active_workspace().focus_down() {
self.switch_workspace_down();
} else {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.switch_workspace_down();
} else {
workspace.focus_down();
}
}
}
pub fn focus_window_or_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
if !self.active_workspace().focus_up() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.switch_workspace_up();
} else {
workspace.focus_up();
}
}
}
@@ -502,26 +494,23 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
return;
}
};
let column = &workspace.columns[workspace.active_column_idx];
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
ActivateWindow::Yes,
removed.width,
removed.is_full_width,
removed.is_floating,
);
}
@@ -532,75 +521,71 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
return;
}
};
let column = &workspace.columns[workspace.active_column_idx];
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
ActivateWindow::Yes,
removed.width,
removed.is_full_width,
removed.is_floating,
);
}
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window {
let source_workspace_idx = if let Some(window) = window {
self.workspaces
.iter()
.enumerate()
.find_map(|(ws_idx, ws)| {
ws.columns.iter().enumerate().find_map(|(col_idx, col)| {
col.tiles
.iter()
.position(|tile| tile.window().id() == window)
.map(|tile_idx| (ws_idx, col_idx, tile_idx))
})
})
.position(|ws| ws.has_window(window))
.unwrap()
} else {
let ws_idx = self.active_workspace_idx;
let ws = &self.workspaces[ws_idx];
if ws.columns.is_empty() {
return;
}
let col_idx = ws.active_column_idx;
let tile_idx = ws.columns[col_idx].active_tile_idx;
(ws_idx, col_idx, tile_idx)
self.active_workspace_idx
};
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let new_id = self.workspaces[new_idx].id();
let activate = window.map_or(true, |win| {
self.active_window().map(|win| win.id()) == Some(win)
});
let activate = if activate {
ActivateWindow::Yes
} else {
ActivateWindow::No
};
let workspace = &mut self.workspaces[source_workspace_idx];
let column = &workspace.columns[col_idx];
let activate = source_workspace_idx == self.active_workspace_idx
&& col_idx == workspace.active_column_idx
&& tile_idx == column.active_tile_idx;
let transaction = Transaction::new();
let removed = if let Some(window) = window {
workspace.remove_tile(window, transaction)
} else if let Some(removed) = workspace.remove_active_tile(transaction) {
removed
} else {
return;
};
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
self.add_window(
new_idx,
removed.tile.into_window(),
self.add_tile(
removed.tile,
MonitorAddWindowTarget::Workspace {
id: new_id,
column_idx: None,
},
activate,
removed.width,
removed.is_full_width,
removed.is_floating,
);
if self.workspace_switch.is_none() {
@@ -617,11 +602,15 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
if workspace.floating_is_active() {
self.move_to_workspace_up();
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
let Some(column) = workspace.remove_active_column() else {
return;
};
self.add_column(new_idx, column, true);
}
@@ -634,11 +623,15 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
if workspace.floating_is_active() {
self.move_to_workspace_down();
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
let Some(column) = workspace.remove_active_column() else {
return;
};
self.add_column(new_idx, column, true);
}
@@ -651,11 +644,15 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
if workspace.floating_is_active() {
self.move_to_workspace(None, idx);
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
let Some(column) = workspace.remove_active_column() else {
return;
};
self.add_column(new_idx, column, true);
}
@@ -705,23 +702,24 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().expel_from_column();
}
pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) {
self.active_workspace().swap_window_in_direction(direction);
}
pub fn center_column(&mut self) {
self.active_workspace().center_column();
}
pub fn focus(&self) -> Option<&W> {
let workspace = &self.workspaces[self.active_workspace_idx];
if !workspace.has_windows() {
return None;
}
let column = &workspace.columns[workspace.active_column_idx];
Some(column.tiles[column.active_tile_idx].window())
pub fn active_window(&self) -> Option<&W> {
self.active_workspace_ref().active_window()
}
pub fn advance_animations(&mut self, current_time: Duration) {
pub fn is_active_fullscreen(&self) -> bool {
self.active_workspace_ref().is_active_fullscreen()
}
pub fn advance_animations(&mut self) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
@@ -729,7 +727,7 @@ impl<W: LayoutElement> Monitor<W> {
}
for ws in &mut self.workspaces {
ws.advance_animations(current_time);
ws.advance_animations();
}
}
@@ -778,19 +776,19 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn update_config(&mut self, options: Rc<Options>) {
for ws in &mut self.workspaces {
ws.update_config(options.clone());
if self.options.empty_workspace_above_first != options.empty_workspace_above_first
&& self.workspaces.len() > 1
{
if options.empty_workspace_above_first {
self.add_workspace_top();
} else if self.workspace_switch.is_none() && self.active_workspace_idx != 0 {
self.workspaces.remove(0);
self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
}
}
if self.options.struts != options.struts {
let scale = self.output.current_scale();
let transform = self.output.current_transform();
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(scale, transform, view_size, working_area);
}
for ws in &mut self.workspaces {
ws.update_config(options.clone());
}
self.options = options;
@@ -809,7 +807,7 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
let mut new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
return;
}
@@ -818,8 +816,12 @@ impl<W: LayoutElement> Monitor<W> {
if new_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && self.active_workspace_idx == 0 {
self.add_workspace_top();
new_idx += 1;
}
let previous_workspace_id = self.previous_workspace_id;
@@ -831,7 +833,7 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn move_workspace_up(&mut self) {
let new_idx = self.active_workspace_idx.saturating_sub(1);
let mut new_idx = self.active_workspace_idx.saturating_sub(1);
if new_idx == self.active_workspace_idx {
return;
}
@@ -840,8 +842,12 @@ impl<W: LayoutElement> Monitor<W> {
if self.active_workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
self.add_workspace_bottom();
}
if self.options.empty_workspace_above_first && new_idx == 0 {
self.add_workspace_top();
new_idx += 1;
}
let previous_workspace_id = self.previous_workspace_id;
@@ -864,7 +870,7 @@ impl<W: LayoutElement> Monitor<W> {
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = offset * size.h;
let clip_rect = Rectangle::from_loc_and_size((0., -offset), size);
let clip_rect = Rectangle::new(Point::from((0., -offset)), size);
rect = rect.intersection(clip_rect)?;
}
@@ -923,7 +929,7 @@ impl<W: LayoutElement> Monitor<W> {
let size = output_size(&self.output);
let (ws, bounds) = self
.workspaces_with_render_positions()
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
.map(|(ws, offset)| (ws, Rectangle::new(offset, size)))
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
Some((ws, bounds.loc))
}
@@ -956,7 +962,8 @@ impl<W: LayoutElement> Monitor<W> {
&'a self,
renderer: &'a mut R,
target: RenderTarget,
) -> impl Iterator<Item = MonitorRenderElement<R>> + '_ {
focus_ring: bool,
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a {
let _span = tracy_client::span!("Monitor::render_elements");
let scale = self.output.current_scale().fractional_scale();
@@ -975,15 +982,20 @@ impl<W: LayoutElement> Monitor<W> {
//
// FIXME: use proper bounds after fixing the Crop element.
let crop_bounds = if self.workspace_switch.is_some() {
Rectangle::from_loc_and_size((-i32::MAX / 2, 0), (i32::MAX, height))
Rectangle::new(
Point::from((-i32::MAX / 2, 0)),
Size::from((i32::MAX, height)),
)
} else {
Rectangle::from_loc_and_size((-i32::MAX / 2, -i32::MAX / 2), (i32::MAX, i32::MAX))
Rectangle::new(
Point::from((-i32::MAX / 2, -i32::MAX / 2)),
Size::from((i32::MAX, i32::MAX)),
)
};
self.workspaces_with_render_positions()
.flat_map(move |(ws, offset)| {
ws.render_elements(renderer, target)
.into_iter()
ws.render_elements(renderer, target, focus_ring)
.filter_map(move |elem| {
CropRenderElement::from_element(elem, scale, crop_bounds)
})
@@ -1062,7 +1074,7 @@ impl<W: LayoutElement> Monitor<W> {
return false;
};
if is_touchpad.map_or(false, |x| gesture.is_touchpad != x) {
if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) {
return false;
}
@@ -1099,6 +1111,7 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
self.clock.clone(),
gesture.current_idx,
new_idx as f64,
velocity,
+2 -5
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -41,9 +40,7 @@ impl OpenAnimation {
}
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn advance_animations(&mut self) {}
pub fn is_done(&self) -> bool {
self.anim.is_done()
@@ -75,7 +72,7 @@ impl OpenAnimation {
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
let mut area = Rectangle::new(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
let mut target_size = area.size.upscale(1.5);
File diff suppressed because it is too large Load Diff
+142 -41
View File
@@ -1,5 +1,4 @@
use std::rc::Rc;
use std::time::Duration;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
@@ -10,10 +9,10 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac,
RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::Animation;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
@@ -49,8 +48,27 @@ pub struct Tile<W: LayoutElement> {
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<f64, Logical>,
/// Whether the tile should float upon unfullscreening.
pub(super) unfullscreen_to_floating: bool,
/// The size that the window should assume when going floating.
///
/// This is generally the last size the window had when it was floating. It can be unknown if
/// the window starts out in the tiling layout or fullscreen.
pub(super) floating_window_size: Option<Size<i32, Logical>>,
/// The position that the tile should assume when going floating, relative to the floating
/// space working area.
///
/// This is generally the last position the tile had when it was floating. It can be unknown if
/// the window starts out in the tiling layout.
pub(super) floating_pos: Option<Point<f64, SizeFrac>>,
/// Currently selected preset width index when this tile is floating.
pub(super) floating_preset_width_idx: Option<usize>,
/// Currently selected preset height index when this tile is floating.
pub(super) floating_preset_height_idx: Option<usize>,
/// The animation upon opening a window.
open_animation: Option<OpenAnimation>,
@@ -73,9 +91,17 @@ pub struct Tile<W: LayoutElement> {
/// Extra damage for clipped surface corner radius changes.
rounded_corner_damage: RoundedCornerDamage,
/// The view size for the tile's workspace.
///
/// Used as the fullscreen target size.
view_size: Size<f64, Logical>,
/// Scale of the output the tile is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
pub(super) options: Rc<Options>,
}
@@ -110,18 +136,29 @@ struct MoveAnimation {
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
pub fn new(
window: W,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
options: Rc<Options>,
) -> Self {
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 is_fullscreen = window.is_fullscreen();
Self {
window,
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
fullscreen_size: Default::default(),
is_fullscreen,
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
unfullscreen_to_floating: false,
floating_window_size: None,
floating_pos: None,
floating_preset_width_idx: None,
floating_preset_height_idx: None,
open_animation: None,
resize_animation: None,
move_x_animation: None,
@@ -129,12 +166,28 @@ impl<W: LayoutElement> Tile<W> {
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
view_size,
scale,
clock,
options,
}
}
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
pub fn update_config(
&mut self,
view_size: Size<f64, Logical>,
scale: f64,
options: Rc<Options>,
) {
// If preset widths or heights changed, clear our stored preset index.
if self.options.preset_column_widths != options.preset_column_widths {
self.floating_preset_width_idx = None;
}
if self.options.preset_window_heights != options.preset_window_heights {
self.floating_preset_height_idx = None;
}
self.view_size = view_size;
self.scale = scale;
self.options = options;
@@ -147,6 +200,8 @@ impl<W: LayoutElement> Tile<W> {
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
self.fullscreen_backdrop.resize(view_size);
}
pub fn update_shaders(&mut self) {
@@ -155,10 +210,7 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn update_window(&mut self) {
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0., 0.)) {
self.is_fullscreen = self.window.is_fullscreen();
}
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() {
@@ -180,7 +232,13 @@ impl<W: LayoutElement> Tile<W> {
let change = self.window.size().to_f64().to_point() - size_from.to_point();
let change = f64::max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
let anim = Animation::new(
self.clock.clone(),
0.,
1.,
0.,
self.options.animations.window_resize.anim,
);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
@@ -208,29 +266,25 @@ impl<W: LayoutElement> Tile<W> {
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self, current_time: Duration) {
pub fn advance_animations(&mut self) {
if let Some(open) = &mut self.open_animation {
open.advance_animations(current_time);
if open.is_done() {
self.open_animation = None;
}
}
if let Some(resize) = &mut self.resize_animation {
resize.anim.set_current_time(current_time);
if resize.anim.is_done() {
self.resize_animation = None;
}
}
if let Some(move_) = &mut self.move_x_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_x_animation = None;
}
}
if let Some(move_) = &mut self.move_y_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_y_animation = None;
}
@@ -264,7 +318,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_window_size(),
is_active,
!draw_border_with_background,
Rectangle::from_loc_and_size(
Rectangle::new(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
),
@@ -316,6 +370,7 @@ impl<W: LayoutElement> Tile<W> {
pub fn start_open_animation(&mut self) {
self.open_animation = Some(OpenAnimation::new(Animation::new(
self.clock.clone(),
0.,
1.,
0.,
@@ -343,7 +398,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_x_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
self.move_x_animation = Some(MoveAnimation {
anim,
@@ -362,7 +417,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_y_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
self.move_y_animation = Some(MoveAnimation {
anim,
@@ -383,10 +438,6 @@ impl<W: LayoutElement> Tile<W> {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
pub fn is_fullscreen(&self) -> bool {
self.is_fullscreen
}
@@ -411,7 +462,7 @@ impl<W: LayoutElement> Tile<W> {
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window_size();
let target_size = self.fullscreen_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.
@@ -441,8 +492,27 @@ impl<W: LayoutElement> Tile<W> {
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.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
let mut size = self.window_expected_or_current_size();
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);
return size;
}
@@ -462,6 +532,15 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
let size = self.window.expected_size();
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
size
}
fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
@@ -485,8 +564,8 @@ impl<W: LayoutElement> Tile<W> {
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.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
@@ -511,7 +590,7 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
let activation_region = Rectangle::from_size(self.tile_size());
activation_region.contains(point)
}
@@ -567,10 +646,9 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size.to_i32_round());
pub fn request_fullscreen(&mut self) {
self.window
.request_fullscreen(self.view_size.to_i32_round());
}
pub fn min_size(&self) -> Size<f64, Logical> {
@@ -633,7 +711,7 @@ impl<W: LayoutElement> Tile<W> {
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
let area = Rectangle::new(window_render_loc, animated_window_size);
let rules = self.window.rules();
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
@@ -732,7 +810,7 @@ impl<W: LayoutElement> Tile<W> {
.window
.render(renderer, window_render_loc, scale, alpha, target);
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
let geo = Rectangle::new(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
@@ -774,12 +852,12 @@ impl<W: LayoutElement> Tile<W> {
if radius != CornerRadius::default() && has_border_shader {
return BorderRenderElement::new(
geo.size,
Rectangle::from_loc_and_size((0., 0.), geo.size),
Rectangle::from_size(geo.size),
GradientInterpolation::default(),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_loc_and_size((0., 0.), geo.size),
Rectangle::from_size(geo.size),
0.,
radius,
scale.x as f32,
@@ -916,4 +994,27 @@ impl<W: LayoutElement> Tile<W> {
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
self.unmap_snapshot.take()
}
pub fn options(&self) -> &Rc<Options> {
&self.options
}
#[cfg(test)]
pub fn view_size(&self) -> Size<f64, Logical> {
self.view_size
}
#[cfg(test)]
pub fn verify_invariants(&self) {
use approx::assert_abs_diff_eq;
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
let scale = self.scale;
let size = self.tile_size();
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
}
}
+1083 -3630
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -11,6 +11,7 @@ pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layer;
pub mod layout;
pub mod niri;
pub mod protocols;
@@ -27,3 +28,6 @@ pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
#[cfg(test)]
mod tests;
+3 -10
View File
@@ -11,7 +11,6 @@ use std::{env, mem};
use clap::Parser;
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
@@ -164,13 +163,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
})
.unwrap_or_default();
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
@@ -184,6 +176,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
false,
)
.unwrap();
@@ -244,10 +237,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Spawn commands from cli and auto-start.
spawn(cli.command);
spawn(cli.command, None);
for elem in spawn_at_startup {
spawn(elem.command);
spawn(elem.command, None);
}
// Show the config error notification right away if needed.
+427 -145
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -177,7 +177,6 @@ where
}
// Verify that there's no more data.
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
{
match file.read(&mut [0]) {
Ok(0) => (),
+2 -2
View File
@@ -206,9 +206,9 @@ where
let output_transform = output.current_transform();
let output_physical_size =
output_transform.transform_size(output.current_mode().unwrap().size);
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
let output_rect = Rectangle::from_size(output_physical_size);
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let rect = Rectangle::new(Point::from((x, y)), Size::from((width, height)));
let output_scale = output.current_scale().fractional_scale();
let physical_rect = rect.to_physical_precise_round(output_scale);
+29 -22
View File
@@ -11,7 +11,7 @@ use anyhow::Context as _;
use calloop::timer::{TimeoutAction, Timer};
use calloop::RegistrationToken;
use pipewire::context::Context;
use pipewire::core::Core;
use pipewire::core::{Core, PW_ID_CORE};
use pipewire::main_loop::MainLoop;
use pipewire::properties::Properties;
use pipewire::spa::buffer::DataType;
@@ -41,7 +41,7 @@ use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Scale, Size, Transform};
use zbus::SignalContext;
use zbus::object_server::SignalEmitter;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State;
@@ -54,12 +54,14 @@ const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
pub struct PipeWire {
_context: Context,
pub core: Core,
pub token: RegistrationToken,
to_niri: calloop::channel::Sender<PwToNiri>,
}
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw(CastTarget),
FatalError,
}
pub struct Cast {
@@ -134,15 +136,26 @@ macro_rules! make_params {
}
impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
pub fn new(
event_loop: &LoopHandle<'static, State>,
to_niri: calloop::channel::Sender<PwToNiri>,
) -> anyhow::Result<Self> {
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
let to_niri_ = to_niri.clone();
let listener = core
.add_listener_local()
.error(|id, seq, res, message| {
.error(move |id, seq, res, message| {
warn!(id, seq, res, message, "pw error");
// Reset PipeWire on connection errors.
if id == PW_ID_CORE && res == -32 {
if let Err(err) = to_niri_.send(PwToNiri::FatalError) {
warn!("error sending FatalError to niri: {err:?}");
}
}
})
.register();
mem::forget(listener);
@@ -154,7 +167,7 @@ impl PipeWire {
}
}
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
event_loop
let token = event_loop
.insert_source(generic, move |_, wrapper, _| {
let _span = tracy_client::span!("pipewire iteration");
wrapper.0.loop_().iterate(Duration::ZERO);
@@ -162,17 +175,10 @@ impl PipeWire {
})
.unwrap();
let (to_niri, from_pipewire) = calloop::channel::channel();
event_loop
.insert_source(from_pipewire, move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
calloop::channel::Event::Closed => (),
})
.unwrap();
Ok(Self {
_context: context,
core,
token,
to_niri,
})
}
@@ -188,7 +194,7 @@ impl PipeWire {
refresh: u32,
alpha: bool,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
signal_ctx: SignalEmitter<'static>,
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
@@ -769,7 +775,11 @@ impl Cast {
let timer = Timer::from_duration(duration);
let token = event_loop
.insert_source(timer, move |_, _, state| {
state.niri.queue_redraw(&output);
// Guard against output disconnecting before the timer has a chance to run.
if state.niri.output_state.contains_key(&output) {
state.niri.queue_redraw(&output);
}
TimeoutAction::Drop
})
.unwrap();
@@ -834,12 +844,9 @@ impl Cast {
return false;
}
let mut buffer = match self.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
warn!("no available buffer in pw stream, skipping frame");
return false;
}
let Some(mut buffer) = self.stream.dequeue_buffer() else {
warn!("no available buffer in pw stream, skipping frame");
return false;
};
let fd = buffer.datas_mut()[0].as_raw().fd;
@@ -1023,7 +1030,7 @@ fn allocate_buffer(
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
.context("error creating GBM buffer object")?;
let modifier = bo.modifier().unwrap();
let modifier = bo.modifier();
let buffer = GbmBuffer::from_bo(bo, false);
Ok((buffer, modifier))
}
+12 -12
View File
@@ -6,7 +6,7 @@ use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::damage::ExtraDamage;
use super::renderer::{AsGlesFrame as _, NiriRenderer};
@@ -117,21 +117,21 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
let bottom_left = corner_radius.bottom_left as f64;
[
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
Rectangle::from_loc_and_size(
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
(top_right, top_right),
Rectangle::new(geo.loc, Size::from((top_left, top_left))),
Rectangle::new(
Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)),
Size::from((top_right, top_right)),
),
Rectangle::from_loc_and_size(
(
Rectangle::new(
Point::from((
geo.loc.x + geo.size.w - bottom_right,
geo.loc.y + geo.size.h - bottom_right,
),
(bottom_right, bottom_right),
)),
Size::from((bottom_right, bottom_right)),
),
Rectangle::from_loc_and_size(
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
(bottom_left, bottom_left),
Rectangle::new(
Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)),
Size::from((bottom_left, bottom_left)),
),
]
}
+1 -1
View File
@@ -54,7 +54,7 @@ impl Element for ExtraDamage {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
Rectangle::from_size(Size::from((1., 1.)))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
+7 -2
View File
@@ -113,6 +113,11 @@ impl<E> SplitElements<E> {
popups.extend(normal);
popups
}
pub fn extend(&mut self, other: SplitElements<E>) {
self.popups.extend(other.popups);
self.normal.extend(other.normal);
}
}
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
@@ -211,7 +216,7 @@ pub fn render_and_download(
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.copy_framebuffer(Rectangle::from_size(buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
@@ -295,7 +300,7 @@ fn render_elements(
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let transform = transform.invert();
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
let output_rect = Rectangle::from_size(transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
+2 -2
View File
@@ -46,7 +46,7 @@ impl AsGlesRenderer for GlesRenderer {
}
}
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
impl AsGlesRenderer for TtyRenderer<'_> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
@@ -66,7 +66,7 @@ impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
}
}
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
impl<'frame> AsGlesFrame<'frame> for TtyFrame<'_, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
+1 -1
View File
@@ -43,7 +43,7 @@ impl ResizeRenderElement {
let tex_next_geo_scaled = tex_next_geo.to_f64().upscale(scale_next);
let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled).to_i32_up();
let area = Rectangle::from_loc_and_size(
let area = Rectangle::new(
area.loc + combined_geo.loc.to_logical(scale),
combined_geo.size.to_logical(scale),
);
+4 -4
View File
@@ -193,7 +193,7 @@ impl ShaderRenderElement {
program,
id: Id::new(),
commit_counter: CommitCounter::default(),
area: Rectangle::from_loc_and_size((0., 0.), size),
area: Rectangle::from_size(size),
opaque_regions: opaque_regions.unwrap_or_default(),
scale,
alpha,
@@ -255,7 +255,7 @@ impl Element for ShaderRenderElement {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
Rectangle::from_size(Size::from((1., 1.)))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -314,7 +314,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
[
rect.loc.x as f32,
rect.loc.y as f32,
@@ -334,7 +334,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size);
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
(0..6).flat_map(move |_| {
[
+3 -4
View File
@@ -85,7 +85,7 @@ impl SolidColorRenderElement {
alpha: f32,
kind: Kind,
) -> Self {
let geo = Rectangle::from_loc_and_size(location, buffer.size());
let geo = Rectangle::new(location.into(), buffer.size());
let color = buffer.color * alpha;
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
}
@@ -125,7 +125,7 @@ impl Element for SolidColorRenderElement {
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
Rectangle::from_size(Size::from((1., 1.)))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -134,8 +134,7 @@ impl Element for SolidColorRenderElement {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if self.color.is_opaque() {
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
.to_physical_precise_down(scale);
let rect = Rectangle::from_size(self.geometry.size).to_physical_precise_down(scale);
OpaqueRegions::from_slice(&[rect])
} else {
OpaqueRegions::default()
+2 -4
View File
@@ -157,7 +157,7 @@ impl<T: Texture> Element for TextureRenderElement<T> {
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
let logical_geo = Rectangle::from_loc_and_size(self.location, self.logical_size());
let logical_geo = Rectangle::new(self.location, self.logical_size());
logical_geo.to_physical_precise_round(scale)
}
@@ -174,9 +174,7 @@ impl<T: Texture> Element for TextureRenderElement<T> {
&self.buffer.logical_size(),
)
})
.unwrap_or_else(|| {
Rectangle::from_loc_and_size((0, 0), self.buffer.texture.size()).to_f64()
})
.unwrap_or_else(|| Rectangle::from_size(self.buffer.texture.size()).to_f64())
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
+536
View File
@@ -0,0 +1,536 @@
use std::cmp::min;
use std::collections::HashMap;
use std::ffi::OsStr;
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;
use single_pixel_buffer::v1::client::wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1;
use smithay::reexports::wayland_protocols::wp::single_pixel_buffer;
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
use smithay::reexports::wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter;
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_surface::{self, XdgSurface};
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_toplevel::{self, XdgToplevel};
use smithay::reexports::wayland_protocols::xdg::shell::client::xdg_wm_base::{self, XdgWmBase};
use wayland_backend::client::Backend;
use wayland_client::globals::Global;
use wayland_client::protocol::wl_buffer::{self, WlBuffer};
use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_compositor::WlCompositor;
use wayland_client::protocol::wl_display::WlDisplay;
use wayland_client::protocol::wl_output::{self, WlOutput};
use wayland_client::protocol::wl_registry::{self, WlRegistry};
use wayland_client::protocol::wl_surface::{self, WlSurface};
use wayland_client::{Connection, Dispatch, Proxy as _, QueueHandle};
use crate::utils::id::IdCounter;
pub struct Client {
pub id: ClientId,
pub event_loop: EventLoop<'static, State>,
pub connection: Connection,
pub qh: QueueHandle<State>,
pub display: WlDisplay,
pub state: State,
}
pub struct State {
pub qh: QueueHandle<State>,
pub globals: Vec<Global>,
pub outputs: HashMap<WlOutput, String>,
pub compositor: Option<WlCompositor>,
pub xdg_wm_base: Option<XdgWmBase>,
pub spbm: Option<WpSinglePixelBufferManagerV1>,
pub viewporter: Option<WpViewporter>,
pub windows: Vec<Window>,
}
pub struct Window {
pub qh: QueueHandle<State>,
pub spbm: WpSinglePixelBufferManagerV1,
pub surface: WlSurface,
pub xdg_surface: XdgSurface,
pub xdg_toplevel: XdgToplevel,
pub viewport: WpViewport,
pub pending_configure: Configure,
pub configures_received: Vec<(u32, Configure)>,
pub close_requsted: bool,
pub configures_looked_at: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Configure {
pub size: (i32, i32),
pub bounds: Option<(i32, i32)>,
pub states: Vec<xdg_toplevel::State>,
}
#[derive(Default)]
pub struct SyncData {
pub done: AtomicBool,
}
static CLIENT_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClientId(u64);
impl ClientId {
fn next() -> ClientId {
ClientId(CLIENT_ID_COUNTER.next())
}
}
impl fmt::Display for Configure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "size: {} × {}, ", self.size.0, self.size.1)?;
if let Some(bounds) = self.bounds {
write!(f, "bounds: {} × {}, ", bounds.0, bounds.1)?;
} else {
write!(f, "bounds: none, ")?;
}
write!(f, "states: {:?}", self.states)?;
Ok(())
}
}
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 {
let id = ClientId::next();
let event_loop = EventLoop::try_new().unwrap();
let connection = connect(socket_name);
let queue = connection.new_event_queue();
let qh = queue.handle();
WaylandSource::new(connection.clone(), queue)
.insert(event_loop.handle())
.unwrap();
let display = connection.display();
let _registry = display.get_registry(&qh, ());
connection.flush().unwrap();
let state = State {
qh: qh.clone(),
globals: Vec::new(),
outputs: HashMap::new(),
compositor: None,
xdg_wm_base: None,
spbm: None,
viewporter: None,
windows: Vec::new(),
};
Self {
id,
event_loop,
connection,
qh,
display,
state,
}
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
}
pub fn send_sync(&self) -> Arc<SyncData> {
let data = Arc::new(SyncData::default());
self.display.sync(&self.qh, data.clone());
self.connection.flush().unwrap();
data
}
pub fn create_window(&mut self) -> &mut Window {
self.state.create_window()
}
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
self.state.window(surface)
}
pub fn output(&mut self, name: &str) -> WlOutput {
self.state
.outputs
.iter()
.find(|(_, v)| *v == name)
.unwrap()
.0
.clone()
}
}
impl State {
pub fn create_window(&mut self) -> &mut Window {
let compositor = self.compositor.as_ref().unwrap();
let xdg_wm_base = self.xdg_wm_base.as_ref().unwrap();
let viewporter = self.viewporter.as_ref().unwrap();
let surface = compositor.create_surface(&self.qh, ());
let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &self.qh, ());
let xdg_toplevel = xdg_surface.get_toplevel(&self.qh, ());
let viewport = viewporter.get_viewport(&surface, &self.qh, ());
let window = Window {
qh: self.qh.clone(),
spbm: self.spbm.clone().unwrap(),
surface,
xdg_surface,
xdg_toplevel,
viewport,
pending_configure: Configure::default(),
configures_received: Vec::new(),
close_requsted: false,
configures_looked_at: 0,
};
self.windows.push(window);
self.windows.last_mut().unwrap()
}
pub fn window(&mut self, surface: &WlSurface) -> &mut Window {
self.windows
.iter_mut()
.find(|w| w.surface == *surface)
.unwrap()
}
}
impl Window {
pub fn commit(&self) {
self.surface.commit();
}
pub fn ack_last(&self) {
let serial = self.configures_received.last().unwrap().0;
self.xdg_surface.ack_configure(serial);
}
pub fn ack_last_and_commit(&self) {
self.ack_last();
self.commit();
}
pub fn attach_new_buffer(&self) {
let buffer = self.spbm.create_u32_rgba_buffer(0, 0, 0, 0, &self.qh, ());
self.surface.attach(Some(&buffer), 0, 0);
}
pub fn set_size(&self, w: u16, h: u16) {
self.viewport.set_destination(i32::from(w), i32::from(h));
}
pub fn set_fullscreen(&self, output: Option<&WlOutput>) {
self.xdg_toplevel.set_fullscreen(output);
}
pub fn unset_fullscreen(&self) {
self.xdg_toplevel.unset_fullscreen();
}
pub fn set_parent(&self, parent: Option<&XdgToplevel>) {
self.xdg_toplevel.set_parent(parent);
}
pub fn set_title(&self, title: &str) {
self.xdg_toplevel.set_title(title.to_owned());
}
pub fn recent_configures(&mut self) -> impl Iterator<Item = &Configure> {
let start = self.configures_looked_at;
self.configures_looked_at = self.configures_received.len();
self.configures_received[start..].iter().map(|(_, c)| c)
}
pub fn format_recent_configures(&mut self) -> String {
let mut buf = String::new();
for configure in self.recent_configures() {
if !buf.is_empty() {
buf.push('\n');
}
write!(buf, "{configure}").unwrap();
}
buf
}
}
impl Dispatch<WlCallback, Arc<SyncData>> for State {
fn event(
_state: &mut Self,
_proxy: &WlCallback,
event: <WlCallback as wayland_client::Proxy>::Event,
data: &Arc<SyncData>,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_callback::Event::Done { .. } => data.done.store(true, Ordering::Relaxed),
_ => unreachable!(),
}
}
}
impl Dispatch<WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &WlRegistry,
event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global {
name,
interface,
version,
} => {
if interface == WlCompositor::interface().name {
let version = min(version, WlCompositor::interface().version);
state.compositor = Some(registry.bind(name, version, qh, ()));
} else if interface == XdgWmBase::interface().name {
let version = min(version, XdgWmBase::interface().version);
state.xdg_wm_base = Some(registry.bind(name, version, qh, ()));
} else if interface == WpSinglePixelBufferManagerV1::interface().name {
let version = min(version, WpSinglePixelBufferManagerV1::interface().version);
state.spbm = Some(registry.bind(name, version, qh, ()));
} else if interface == WpViewporter::interface().name {
let version = min(version, WpViewporter::interface().version);
state.viewporter = Some(registry.bind(name, version, qh, ()));
} else if interface == WlOutput::interface().name {
let version = min(version, WlOutput::interface().version);
let output = registry.bind(name, version, qh, ());
state.outputs.insert(output, String::new());
}
let global = Global {
name,
interface,
version,
};
state.globals.push(global);
}
wl_registry::Event::GlobalRemove { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
output: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_output::Event::Geometry { .. } => (),
wl_output::Event::Mode { .. } => (),
wl_output::Event::Done => (),
wl_output::Event::Scale { .. } => (),
wl_output::Event::Name { name } => {
*state.outputs.get_mut(output).unwrap() = name;
}
wl_output::Event::Description { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlCompositor, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlCompositor,
_event: <WlCompositor as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<XdgWmBase, ()> for State {
fn event(
_state: &mut Self,
xdg_wm_base: &XdgWmBase,
event: <XdgWmBase as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
xdg_wm_base::Event::Ping { serial } => {
xdg_wm_base.pong(serial);
}
_ => unreachable!(),
}
}
}
impl Dispatch<WlSurface, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlSurface,
event: <WlSurface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_surface::Event::Enter { .. } => (),
wl_surface::Event::Leave { .. } => (),
wl_surface::Event::PreferredBufferScale { .. } => (),
wl_surface::Event::PreferredBufferTransform { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<XdgSurface, ()> for State {
fn event(
state: &mut Self,
xdg_surface: &XdgSurface,
event: <XdgSurface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
xdg_surface::Event::Configure { serial } => {
let window = state
.windows
.iter_mut()
.find(|w| w.xdg_surface == *xdg_surface)
.unwrap();
let configure = window.pending_configure.clone();
window.configures_received.push((serial, configure));
}
_ => unreachable!(),
}
}
}
impl Dispatch<XdgToplevel, ()> for State {
fn event(
state: &mut Self,
xdg_toplevel: &XdgToplevel,
event: <XdgToplevel as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
let window = state
.windows
.iter_mut()
.find(|w| w.xdg_toplevel == *xdg_toplevel)
.unwrap();
match event {
xdg_toplevel::Event::Configure {
width,
height,
states,
} => {
let configure = &mut window.pending_configure;
configure.size = (width, height);
configure.states = states
.chunks_exact(4)
.flat_map(TryInto::<[u8; 4]>::try_into)
.map(u32::from_ne_bytes)
.flat_map(xdg_toplevel::State::try_from)
.collect();
}
xdg_toplevel::Event::Close => {
window.close_requsted = true;
}
xdg_toplevel::Event::ConfigureBounds { width, height } => {
window.pending_configure.bounds = Some((width, height));
}
xdg_toplevel::Event::WmCapabilities { .. } => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WlBuffer, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlBuffer,
event: <WlBuffer as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
wl_buffer::Event::Release => (),
_ => unreachable!(),
}
}
}
impl Dispatch<WpSinglePixelBufferManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpSinglePixelBufferManagerV1,
_event: <WpSinglePixelBufferManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<WpViewporter, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpViewporter,
_event: <WpViewporter as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
impl Dispatch<WpViewport, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpViewport,
_event: <WpViewport as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
unreachable!()
}
}
+139
View File
@@ -0,0 +1,139 @@
use std::os::fd::AsFd as _;
use std::sync::atomic::Ordering;
use std::time::Duration;
use calloop::generic::Generic;
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
use niri_config::Config;
use smithay::output::Output;
use super::client::{Client, ClientId};
use super::server::Server;
use crate::niri::Niri;
pub struct Fixture {
pub event_loop: EventLoop<'static, State>,
pub handle: LoopHandle<'static, State>,
pub state: State,
}
pub struct State {
pub server: Server,
pub clients: Vec<Client>,
}
impl Fixture {
pub fn new() -> Self {
Self::with_config(Config::default())
}
pub fn with_config(config: Config) -> Self {
let event_loop = EventLoop::try_new().unwrap();
let handle = event_loop.handle();
let server = Server::new(config);
let fd = server.event_loop.as_fd().try_clone_to_owned().unwrap();
let source = Generic::new(fd, Interest::READ, Mode::Level);
handle
.insert_source(source, |_, _, state: &mut State| {
state.server.dispatch();
Ok(PostAction::Continue)
})
.unwrap();
let state = State {
server,
clients: Vec::new(),
};
Self {
event_loop,
handle,
state,
}
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
}
pub fn niri_state(&mut self) -> &mut crate::niri::State {
&mut self.state.server.state
}
pub fn niri(&mut self) -> &mut Niri {
&mut self.niri_state().niri
}
pub fn niri_output(&self, n: u8) -> Output {
let niri = &self.state.server.state.niri;
let idx = usize::from(n - 1);
let output = niri.global_space.outputs().nth(idx).unwrap();
output.clone()
}
pub fn niri_focus_output(&mut self, n: u8) {
let niri = &mut self.state.server.state.niri;
let idx = usize::from(n - 1);
let output = niri.global_space.outputs().nth(idx).unwrap();
niri.layout.focus_output(output);
}
pub fn add_output(&mut self, n: u8, size: (u16, u16)) {
let state = self.niri_state();
let niri = &mut state.niri;
state.backend.headless().add_output(niri, n, size);
}
pub fn add_client(&mut self) -> ClientId {
let client = Client::new(&self.state.server.state.niri.socket_name);
let id = client.id;
let fd = client.event_loop.as_fd().try_clone_to_owned().unwrap();
let source = Generic::new(fd, Interest::READ, Mode::Level);
self.handle
.insert_source(source, move |_, _, state: &mut State| {
state.client(id).dispatch();
Ok(PostAction::Continue)
})
.unwrap();
self.state.clients.push(client);
self.roundtrip(id);
id
}
pub fn client(&mut self, id: ClientId) -> &mut Client {
self.state.client(id)
}
pub fn roundtrip(&mut self, id: ClientId) {
let client = self.state.client(id);
let data = client.send_sync();
while !data.done.load(Ordering::Relaxed) {
self.dispatch();
}
}
/// Rountrip 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.
///
/// I suspect that this is because these configure events are sent from the niri loop callback,
/// so they arrive after the sync done event and don't get processed in that client dispatch
/// cycle. I'm not sure why this would be dependent on multithreading. But if this is indeed
/// the issue, then a double roundtrip fixes it.
pub fn double_roundtrip(&mut self, id: ClientId) {
self.roundtrip(id);
self.roundtrip(id);
}
}
impl State {
pub fn client(&mut self, id: ClientId) -> &mut Client {
self.clients.iter_mut().find(|c| c.id == id).unwrap()
}
}
+856
View File
@@ -0,0 +1,856 @@
use client::ClientId;
use insta::assert_snapshot;
use niri_config::Config;
use niri_ipc::SizeChange;
use smithay::utils::Point;
use wayland_client::protocol::wl_surface::WlSurface;
use super::*;
// 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 unfocus_preserves_current_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.roundtrip(id);
// Change window size while it's floating.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Focus a different output which should drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request 200 × 200 because that's the current window size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
);
// Change window size again.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// Focus the first output which should add back the Activated state.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request 300 × 300 because that's the current window size.
let window = f.client(id).window(&surface);
assert_snapshot!(
window.format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_different_size() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the Activated state change configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
f.niri().layout.toggle_window_floating(None);
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size, 500 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Focus a different output which should drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size since the window hasn't committed yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Ack but don't commit yet.
let window = f.client(id).window(&surface);
window.ack_last();
f.roundtrip(id);
// Add the activated state.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the new size since the window hasn't committed yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Commit but with some different size.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Drop the Activated state.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the current window size rather than keep requesting 500 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: []"
);
}
#[test]
fn set_window_width_uses_current_height() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different on both axes.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a width change.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should use the current window height (200), rather than the initial window height (100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn set_window_height_uses_current_width() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different on both axes.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a width change.
f.niri()
.layout
.set_window_height(None, SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should use the current window width (200), rather than the initial window width (100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 500, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_same_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Resize to something different.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a size change to the same size.
f.niri().layout.set_column_width(SizeChange::SetFixed(200));
f.double_roundtrip(id);
// This needn't request anything because we're already that size; the size in the current
// server state matches the requested size.
//
// FIXME: However, currently it will request the size anyway because the code checks the
// current server state, and the last size niri requested of the window was 100×100 (even if
// the window already acked and committed in response).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_to_different_then_same() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to any configure from the floating change.
let window = f.client(id).window(&surface);
window.ack_last_and_commit();
f.roundtrip(id);
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Before the window has a chance to respond, request a size change to the same, new size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
// And also drop the Activated state to have some pending change.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should keep requesting the new size (500 × 100) since the window has not responded to
// it yet.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Commit in response to the size change request.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// And also add the Activated state to have some pending change.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the current window size (300 × 300) since the window has committed in
// response to the size change.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn restore_floating_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit in response to the floating configure.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Change back to tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a tiling size configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Resize as requested.
let window = f.client(id).window(&surface);
let (_, configure) = window.configures_received.last().unwrap();
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
window.ack_last_and_commit();
f.roundtrip(id);
// Change back to floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a configure restoring out previous 200 × 200 size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn moving_across_workspaces_doesnt_cancel_resize() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit in response to the floating configure.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: [Activated]"
);
// Move to a different workspace before the window has a chance to respond. This will remove it
// from one floating layout and add into a different one, potentially causing a size request.
f.niri().layout.move_to_workspace_down();
// Drop the Activated state to force a configure.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size again (500 × 200) since the window hasn't responded to it.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 200, bounds: 1920 × 1080, states: []"
);
// Respond to the resize with a different size.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.ack_last_and_commit();
f.roundtrip(id);
// Focus, adding Activated, and move to workspace down, causing removing and adding to a
// floating layout.
f.niri_focus_output(1);
f.niri().layout.move_to_workspace_down();
f.double_roundtrip(id);
// This should request the current size (300 × 300) since the window responded to the change.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn moving_to_floating_doesnt_cancel_resize() {
let (mut f, id, surface) = set_up();
let _ = f.client(id).window(&surface).recent_configures();
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size (500 ×).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Before the window has a chance to respond, make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should keep requesting the new size (500 ×).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 1048, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn interactive_move_unfullscreen_to_floating_restores_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
niri.layout.set_fullscreen(&window, true);
f.double_roundtrip(id);
// This should request a fullscreen size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 1920 × 1080, bounds: 1888 × 1048, states: [Activated, Fullscreen]"
);
// Start an interactive move which causes an unfullscreen into floating.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
niri.layout
.interactive_move_begin(window.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This should request the stored floating size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_during_interactive_move_propagates_to_floating() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Start an interactive move.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
niri.layout
.interactive_move_begin(window_id.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window_id,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This shouldn't request any new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Change size while we're being interactively moved.
let window = f.client(id).window(&surface);
window.set_size(300, 300);
window.commit();
f.double_roundtrip(id);
// This shouldn't request any new size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// End the interactive move, placing the window into floating.
f.niri().layout.interactive_move_end(&window_id);
f.double_roundtrip(id);
// This should keep the new 300 × 300 size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 300, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn resize_in_steps() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the floating bounds state change configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
// Request a size change to a different size in two steps.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.niri()
.layout
.set_window_height(None, SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the full new size (500 × 500) once.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 500, bounds: 1920 × 1080, states: [Activated]"
);
let window = f.client(id).window(&surface);
let serial = window.configures_received.last().unwrap().0;
// Request a size change now that the previous one is pending-but-not-acked.
f.niri().layout.set_column_width(SizeChange::SetFixed(600));
// Drop Activated to work around resize throttling.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size (600 × 500) once.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 600 × 500, bounds: 1920 × 1080, states: []"
);
// Commit in response to the previous configure.
let window = f.client(id).window(&surface);
window.xdg_surface.ack_configure(serial);
window.set_size(500, 500);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Request a height change now that the first one is committed-to, but the second isn't.
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
f.niri()
.layout
.set_window_height(Some(&window), SizeChange::SetFixed(600));
// Add Activated to work around resize throttling.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should request the latest sizes (600 × 600).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 600 × 600, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn state_change_doesnt_break_use_window_size() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Commit in response to the bounds change that comes with toggling floating.
f.client(id).window(&surface).ack_last_and_commit();
f.roundtrip(id);
// Request a size change to a different size.
f.niri().layout.set_column_width(SizeChange::SetFixed(500));
f.double_roundtrip(id);
// This should request the new size (500 × 100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: [Activated]"
);
let window = f.client(id).window(&surface);
let serial = window.configures_received.last().unwrap().0;
// Request a state change by dropping Activated.
f.niri_focus_output(2);
f.double_roundtrip(id);
// This should request the new size (500 × 100).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 500 × 100, bounds: 1920 × 1080, states: []"
);
// Commit in response to the previous configure with a different size.
let window = f.client(id).window(&surface);
window.xdg_surface.ack_configure(serial);
window.set_size(300, 300);
window.commit();
f.double_roundtrip(id);
// This shouldn't request anything.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
// Request a height change now that the first one is committed-to, but the second isn't.
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window = mapped.window.clone();
f.niri()
.layout
.set_window_height(Some(&window), SizeChange::SetFixed(600));
// Add Activated state to force a configure.
f.niri_focus_output(1);
f.double_roundtrip(id);
// This should already request the current width (300 × 600) rather than the pending previous
// width (500 × 600) from the state change configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 600, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn interactive_move_restores_floating_size_when_set_to_floating() {
let (mut f, id, surface) = set_up();
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// Change size while we're floating and commit to make niri remember it.
let window = f.client(id).window(&surface);
window.set_size(200, 200);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Change back to tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// We should get a tiled size configure.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Resize as requested.
let window = f.client(id).window(&surface);
let (_, configure) = window.configures_received.last().unwrap();
window.set_size(configure.size.0 as u16, configure.size.1 as u16);
window.ack_last_and_commit();
f.roundtrip(id);
// Start an interactive move.
let output = f.niri_output(1);
let niri = f.niri();
let mapped = niri.layout.windows().next().unwrap().1;
let window_id = mapped.window.clone();
niri.layout
.interactive_move_begin(window_id.clone(), &output, Point::default());
niri.layout.interactive_move_update(
&window_id,
Point::from((1000., 0.)),
output,
Point::default(),
);
f.double_roundtrip(id);
// This shouldn't request any new size because interactive move targets tiling.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 1048, bounds: 1920 × 1080, states: [Activated]"
);
// Change interactive move to target floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should restore the floating window size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 200, bounds: 1920 × 1080, states: [Activated]"
);
// End the interactive move, placing the window into floating.
f.niri().layout.interactive_move_end(&window_id);
f.double_roundtrip(id);
// This should keep the floating window size (200 × 200).
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@""
);
}
#[test]
fn floating_doesnt_store_fullscreen_size() {
let mut f = Fixture::new();
f.add_output(1, (1920, 1080));
f.add_output(2, (1280, 720));
// Open a window fullscreen.
let id = f.add_client();
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.set_fullscreen(None);
window.commit();
f.roundtrip(id);
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(1920, 1080);
window.ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should request 0 × 0 to unfullscreen.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 0 × 0, bounds: 1920 × 1080, states: [Activated]"
);
// Without committing, make it tiling again. We never committed while floating, so there's no
// floating size to remember.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should request the tiled size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 1920 × 1048, bounds: 1888 × 1048, states: [Activated]"
);
// Commit in response.
let window = f.client(id).window(&surface);
window.set_size(100, 100);
window.ack_last_and_commit();
f.roundtrip(id);
// Make the window floating again.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This shouldn't request any size change, particularly not the fullscreen size.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 100 × 100, bounds: 1920 × 1080, states: [Activated]"
);
}
#[test]
fn floating_respects_non_fixed_min_max_rule() {
let config = r##"
window-rule {
min-width 200
max-width 300
}
"##;
let config = Config::parse("test.kdl", config).unwrap();
let mut f = Fixture::with_config(config);
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);
// Open with smaller width than min.
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);
// Commit to the Activated state configure.
f.client(id).window(&surface).ack_last_and_commit();
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should clamp to min-width and request 200 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 200 × 100, bounds: 1920 × 1080, states: [Activated]"
);
// Commit with a bigger width than max.
let window = f.client(id).window(&surface);
window.set_size(400, 100);
window.ack_last_and_commit();
f.roundtrip(id);
// Make it tiling.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface).recent_configures();
f.client(id).window(&surface).ack_last_and_commit();
f.roundtrip(id);
// Make it floating.
f.niri().layout.toggle_window_floating(None);
f.double_roundtrip(id);
// This should clamp to max-width and request 300 × 100.
assert_snapshot!(
f.client(id).window(&surface).format_recent_configures(),
@"size: 300 × 100, bounds: 1920 × 1080, states: [Activated]"
);
}
+8
View File
@@ -0,0 +1,8 @@
use fixture::Fixture;
mod client;
mod fixture;
mod server;
mod floating;
mod window_opening;
+37
View File
@@ -0,0 +1,37 @@
use std::time::Duration;
use calloop::EventLoop;
use niri_config::Config;
use smithay::reexports::wayland_server::Display;
use crate::niri::State;
pub struct Server {
pub event_loop: EventLoop<'static, State>,
pub state: State,
}
impl Server {
pub fn new(config: Config) -> Self {
let event_loop = EventLoop::try_new().unwrap();
let handle = event_loop.handle();
let display = Display::new().unwrap();
let state = State::new(
config,
handle.clone(),
event_loop.get_signal(),
display,
true,
)
.unwrap();
Self { event_loop, state }
}
pub fn dispatch(&mut self) {
self.event_loop
.dispatch(Duration::ZERO, &mut self.state)
.unwrap();
self.state.refresh_and_flush_clients();
}
}
@@ -0,0 +1,13 @@
---
source: src/tests/window_opening.rs
description: "config:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 616 × 688, bounds: 1248 × 688, states: [Activated]
@@ -0,0 +1,13 @@
---
source: src/tests/window_opening.rs
description: "set parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,13 @@
---
source: src/tests/window_opening.rs
description: "set parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,13 @@
---
source: src/tests/window_opening.rs
description: "set parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 0 × 0, bounds: 1280 × 720, states: []
post-map configures:
size: 1 × 1, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,13 @@
---
source: src/tests/window_opening.rs
description: "set parent: B2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-2
final workspace: 0 (ws-2)
initial configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
post-map configures:
size: 1 × 1, bounds: 1920 × 1080, states: []
@@ -0,0 +1,17 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,17 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: A2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,17 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: B1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-1\"\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 0 × 0, bounds: 1280 × 720, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 0 × 0, bounds: 1280 × 720, states: [Activated]
@@ -0,0 +1,17 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nset parent: B2\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}\n\nwindow-rule {\n match title=\"parent\"\n open-on-output \"headless-2\"\n}"
expression: snapshot
---
final monitor: headless-2
final workspace: 0 (ws-2)
initial configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1920 × 1080, bounds: 1888 × 1048, states: [Fullscreen]
unfullscreen configure:
size: 0 × 0, bounds: 1920 × 1080, states: []
@@ -0,0 +1,17 @@
---
source: src/tests/window_opening.rs
description: "want fullscreen: A1\nconfig:\nworkspace \"ws-1\" {\n open-on-output \"headless-1\"\n}\n\nworkspace \"ws-2\" {\n open-on-output \"headless-2\"\n}\n\nwindow-rule {\n exclude title=\"parent\"\n\n open-fullscreen false\n}"
expression: snapshot
---
final monitor: headless-1
final workspace: 0 (ws-1)
initial configure:
size: 616 × 688, bounds: 1248 × 688, states: []
post-map configures:
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen]
size: 1280 × 720, bounds: 1248 × 688, states: [Fullscreen, Activated]
unfullscreen configure:
size: 616 × 688, bounds: 1248 × 688, states: [Activated]

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