Compare commits

..

787 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
Ivan Molodetskikh 9d8f640503 niri-ipc: Document features 2024-11-09 17:57:52 +03:00
Ivan Molodetskikh b18cfbae23 niri-ipc: Add README and Cargo.toml metadata 2024-11-09 17:57:34 +03:00
Ivan Molodetskikh f64e7e14c3 Bump version to 0.1.10 2024-11-09 17:35:31 +03:00
Ivan Molodetskikh e8c9bfc06a wiki: Add scroll-button to mouse and touchpad overview 2024-11-09 17:23:59 +03:00
Ivan Molodetskikh 07452f50a8 Update dependencies 2024-11-09 15:57:17 +03:00
Ivan Molodetskikh 642c5acebb wiki: Remove outdated info from Application Issues 2024-11-09 11:04:39 +03:00
Ivan Molodetskikh 0886dedff1 wiki: Mention Xwayland on other pages 2024-11-09 11:04:39 +03:00
Ivan Molodetskikh cc88a7d42e default-config: Bind Ctrl-Alt-Del to quit
This seems to be a shared bind across compositors.
2024-11-09 10:29:13 +03:00
Ivan Molodetskikh c0829087da Lock session right away with no outputs 2024-11-08 16:25:06 +03:00
Ivan Molodetskikh b6f6d6a7c2 wiki: Update getting started 2024-11-08 09:43:43 +03:00
Ivan Molodetskikh 5ff8b89aaf Rework output connection to always go through on_output_config_changed()
This has the following benefits:
1. connector_connected() is now more closely mirroring
   connector_disconnected() in that it merely lights up the connector,
   and doesn't check if the connector should be off from the config.
2. We can use more complex on/off logic that depends on multiple
   connectors. For example, this commit adds logic to only disable the
   laptop panel on lid close if there are other connected outputs.

We don't want to disable the laptop panel on lid close if it's the only
connected output because it causes screen lockers to create their
surface from scratch on normal laptop unsuspend, which is undesirable
and also confuses some screen lockers.
2024-11-08 09:11:56 +03:00
Ivan Molodetskikh 927abad4b4 Only call on_output_config_changed() on lid switch
We don't need to reload the niri output config.
2024-11-08 09:11:28 +03:00
Ivan Molodetskikh 3d31f9860a Extract format_make_model_serial() 2024-11-08 09:10:54 +03:00
Ivan Molodetskikh 8867a4f84c Add disable-monitor-names debug flag 2024-11-06 08:42:22 +03:00
Ivan Molodetskikh 88f4c1d610 layout: Preserve active workspace for removed outputs 2024-11-05 21:52:02 +03:00
Ivan Molodetskikh ddcb5c5e10 layout: Move some types further down 2024-11-05 21:08:50 +03:00
Ivan Molodetskikh cd90dfc7be Disable laptop panel when the lid is closed 2024-11-05 10:03:51 +03:00
Ivan Molodetskikh a778ab3897 Extract is_laptop_panel() to utils 2024-11-05 09:40:12 +03:00
Ivan Molodetskikh 4c2f49d566 wiki: Add Since to switch events 2024-11-03 23:00:18 +03:00
Ivan Molodetskikh 49d7052bb3 wiki: Add trackball section to config overview 2024-11-03 22:58:18 +03:00
Ivan Molodetskikh 07be7e7eae wiki: Add Since to scroll-button 2024-11-03 22:56:49 +03:00
Ivan Molodetskikh 97c8717d1e wiki: Mention insert-hint config on the gestures page 2024-11-03 22:52:49 +03:00
Ivan Molodetskikh 3ac0a751fe wiki: Add Since to scroll-factor 2024-11-03 22:50:15 +03:00
elipp 8b39f986d9 Implement scroll_factor mouse and touchpad setting (#730)
* Implement scroll_factor mouse and touchpad setting

* Change to FloatOrInt, add docs

* Also change v120 values

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-03 18:43:03 +00:00
Christian Meissl 354c365a03 xdg: cleanup activation tokens
valid tokens will stay around until explicitly cleaned-up.
remove the token after it has been successfully used
or we consider it timed out to prevent leaking the memory
used by the activation tokens
2024-11-03 09:13:41 -08:00
Ivan Molodetskikh e0ebf1bdff Remove pointer_grab_ongoing in favor of checking the actual grab 2024-11-03 10:23:21 +03:00
Ivan Molodetskikh 11633aef98 Use is() instead of downcast().is_some() 2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 9193245871 Correct pointer constraint activation logic
Internally it uses the pointer focus, so make sure we have up-to-date
focus before setting it.
2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 7baf10b751 Clarify redraw in refresh_pointer_focus() 2024-11-03 10:15:19 +03:00
Ivan Molodetskikh f5d91c5ecc Rename pointer_focus to pointer_contents, clarify comments
This is not pointer focus and it shouldn't be pointer focus, let's be
clear about it.
2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 69e3edb5a3 Rename surface_under_and_global_space() to contents_under() 2024-11-03 08:50:17 +03:00
LoipesMas d58bb4eaa3 flake: set RUSTFLAGS instead of CARGO_BUILD_RUSTFLAGS 2024-11-02 12:35:04 -07:00
LoipesMas c5fe25f422 flake: libseat has been renamed to seatd 2024-11-02 12:35:04 -07:00
Ivan Molodetskikh 600cffb009 Update Smithay (lock leak fix) 2024-11-02 18:55:56 +03:00
Christian Meissl b9d14a9eda portal: prefer gtk for access portal
using gnome for the access portal does not work,
so just override by directly using the gtk one
2024-11-02 07:55:37 -07:00
Ivan Molodetskikh 0e7e398df3 Replace current_state() with with_toplevel_role()
Avoid microallocations that happen in current_state().
2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 86bdc6898b Add with_toplevel_role() util function 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh e5ca335115 Add Tracy allocation profiling feature flag 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh fce5d66878 Follow window corner radius in insert hint 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 05d218113c Add gradient support for the insert hint
Implement it via FocusRing which already handles SolidColor vs. Border
render element.
2024-11-02 10:53:55 +03:00
Ivan Molodetskikh ef6af6adc1 Change TODO to FIXME 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 6632699e00 Remove obsolete TODO 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh d3e72245b0 Don't show the cursor on programmatic movement
For keyboard-only use, especially with warp-mouse-to-focus, the
intention is that the cursor stays hidden from keyboard and other
automatic actions, and only shows up with an actual mouse movement.
2024-10-29 21:52:03 -07:00
Ivan Molodetskikh 13fe9c8ac3 [cfg-breaking] Rename hide-on-key-press to hide-when-typing
I originally preferred on-key-press, but when-typing feels more natural
and matches sway. This setting had not been in a stable release yet so
this is not stable release cfg breaking.
2024-10-29 21:52:03 -07:00
Ivan Molodetskikh 6ecbf2db8a Deny toplevel move from DnD grabs
Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
2024-10-28 21:12:58 +03:00
Ivan Molodetskikh c9be9056ef Update Smithay 2024-10-28 21:12:58 +03:00
Ivan Molodetskikh 0866990b7d wiki/Gestures: Add interactive move 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh f04befb567 wiki: Document insert-hint config 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh da3e5c4424 Implement touch interactive resize 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh 26ab4dfb87 Implement touch interactive move 2024-10-27 23:07:39 -07:00
Rasmus Eneman e887ee93a3 Implement interactive window move 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh d640e85158 Require Clone for LayoutElement::Id
Now that we have MappedId, this could really be Copy. But it's quite a
big refactor, so for now just require Clone as I'll need it.
2024-10-27 23:07:39 -07:00
gmorer c8044a9b5d ShaderRenderElement use borrowed Uniforms to minimize copy (#756) 2024-10-24 07:42:19 +03:00
Ivan Molodetskikh 289ae3604d tty: Guard against output disappearing immediately after connection
Fixes https://github.com/YaLTeR/niri/issues/739
2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 55fb885256 Use new Smithay method for turning off DPMS 2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 73a531f8bc Update dependencies (wl_output.scale fix) 2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 10f04fd19d layout: Update tile config in Column::add_tile_at() 2024-10-19 12:33:44 +03:00
Christian Meissl 79fd309d6c support binding actions to switches (#747)
* support spawn action on switch events

this adds a new config section named `switch-events`
that allows to bind `spawn` action to certain switch
toggles.

* Expand docs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-18 14:00:40 +00:00
Ivan Molodetskikh dd8b2be044 layout: Add missing active idx check before setting activate prev on removal 2024-10-18 10:06:09 +03:00
Ivan Molodetskikh 8d08782eba Set CLOEXEC on logind inhibit fd
Don't leak it to child processes.
2024-10-17 08:59:06 +03:00
Ivan Molodetskikh 8555f37dbf layout: Use remove_column_by_idx in remove_tile_by_idx 2024-10-17 08:59:06 +03:00
Ivan Molodetskikh 4b837f429c layout: Accept anim_config in remove_column_by_idx 2024-10-17 08:59:06 +03:00
chillinbythetree a480087618 Add scroll-button property for Touchpad, Mouse, Trackpoint, Trackball (#744) 2024-10-17 05:43:47 +00:00
tazjin 84655d3b26 Implement input configuration for trackballs (#743)
* niri-config: add trackball configuration struct

The available options are mostly the same as for mice. I've verified that each
option is applicable to trackballs in the libinput CLI.

* input: apply trackball config settings
2024-10-16 13:51:56 +00:00
Ivan Molodetskikh 40843cbda1 layout/monitor: Extract workspace_under() 2024-10-16 09:39:34 +03:00
Ivan Molodetskikh a13b9298c6 Draw the layout as inactive when layer-shell has focus 2024-10-15 11:11:57 +03:00
Christian Meissl 0c5e046820 input: apply output transform for tablet input (#737)
when mapping a tablet input to an output apply
the output transform just like we already do for
touch input.
2024-10-15 11:11:15 +03:00
Ivan Molodetskikh 907ebc4977 Add boxed_union proptest-derive feature
Our Op enum grew large enough to trigger a stack overflow in
proptest-derive's generated code. Thankfully, this feature works around
the problem.
2024-10-15 10:06:55 +03:00
sodiboo e4161be1bf flake: use nightly rust-analyzer and add rust-src component (#735)
this also improves the application of overlays to be more uniform; what
was previously done was just Wrong
2024-10-15 08:21:49 +03:00
Ivan Molodetskikh be7fbd418f layout: Return Tile + info upon removal 2024-10-14 18:08:44 +03:00
Ivan Molodetskikh 06ec9eecdb layout/tests: Use existing method 2024-10-14 17:39:55 +03:00
Ivan Molodetskikh 79eef5ee90 layout: Remove unnecessary vec lookup 2024-10-14 17:36:00 +03:00
Ivan Molodetskikh 29602ca995 layout: Extract Monitor::workspaces_with_render_positions() 2024-10-14 11:08:44 +03:00
Mark Karlinsky d7156df842 Add support for running as a dinit service (#728)
* Added dinit services

* Added dinit support to niri-session

* Replaced shutdown script for dinit with a single command execution

* Added dinit service files to Getting Started install tables

* Fix typo in resources/dinit/niri

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

* Fixed mistakes in wiki/Getting-Started.md

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

* niri-session does not start dinit anymore

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-13 12:26:16 +00:00
Ivan Molodetskikh 33b39913c7 layout: Fix expel animation of the smaller window in column 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh d5cbc35811 Implement ConsumeOrExpelWindow{Left,Right} by id 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh a038c5aaab layout/workspace: Add add_tile_to_column() 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh c9c985c927 Support empty column in tile_offsets
Will be needed for the new inserting tile code.
2024-10-11 11:00:50 +03:00
Ivan Molodetskikh 859c0be0e5 layout: Add clarifying comment 2024-10-10 10:44:18 +03:00
Ivan Molodetskikh 810ea245f9 layout: Deduplicate default width resolution 2024-10-10 10:40:59 +03:00
Ivan Molodetskikh 58fc5f3b06 layout: Replace move_window_to_output with move_to_output 2024-10-10 10:28:55 +03:00
Ivan Molodetskikh 7d4e99b760 layout/workspace: Reduce code duplication in adding windows 2024-10-10 10:17:16 +03:00
Ivan Molodetskikh ab7d81aae0 layout: Reduce field visibility
The outside code isn't supposed to mess with the fields.
2024-10-10 09:24:20 +03:00
Winter e24723125f added power-on-monitors (#723) 2024-10-09 08:50:06 +00:00
Ivan Molodetskikh 03c603918d Document the new cursor hide settings 2024-10-06 22:09:19 -07:00
Ivan Molodetskikh 6fb60dacd2 Rework pointer inactivity hide as a timer
The previous way was prone to triggering late due to compositor idling
and therefore never calling the check function.
2024-10-06 22:09:19 -07:00
yzy-1 42a9daec9d Implement hide cursor on key press and on timeout 2024-10-06 22:09:19 -07:00
Ivan Molodetskikh 1ba2be3928 Show hidden pointer on mouse press
Feels like this should be the case.
2024-10-06 22:09:19 -07:00
sodiboo 66be000410 implement locked cursor position hints (#685)
* implement cursor position hints

* Remove redundant fully qualified path

* Find root surface

* Convert nesting to if-return

* Manually wrap error messages

* Remove error!() prints

* Add queue redraw

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-06 20:36:49 +03:00
sodiboo 5fc669c282 remove redundant pointer casts in shader code 2024-10-05 22:26:47 -07:00
sodiboo 9b78b15ba5 use CStr literals over calling CStr::from_bytes_with_nul 2024-10-05 22:26:47 -07:00
sodiboo b9fd0a405e use if let Some() over match with None => () 2024-10-05 22:26:47 -07:00
seth 1b44e0cd20 flake: add overlay output 2024-10-05 12:09:24 -07:00
seth b3d4d4eacc flake: use rust-overlay in dev shell
This allows `niri-visual-tests` to still be built and run in the dev
shell where it's necessary, as well as brings back the nightly `rustfmt`
used by the project

We can't use `fenix` again though as it doesn't wrap `ld` like nixpkgs
and rust-overlay do; without it, the way we link `dlopen()`'d libraries
breaks
2024-10-05 12:09:24 -07:00
seth a835bdc940 ci: nix build -> nix flake check
The (debug) package is already set as a check and will still be built
with this, but Nix will now also check other outputs automatically --
such as the dev shell
2024-10-05 12:09:24 -07:00
seth b258fd69d2 flake: improve packaging
Some highlights include:

- Removing some unnecessary dependencies of the package itself
- Allowing for overriding the package
- Adding Cargo feature toggles
- Installing all niri-related resources
- Avoiding `LD_LIBRARY_PATH` hacks
2024-10-05 12:09:24 -07:00
seth 3ab3e778ab flake: drop most external inputs
Previously, inputs like Crane and Fenix were used to only build the
`niri` package. This isn't really required, and can easily be replaced
by nixpkgs' `rustPlatform` -- which will also lead to less dependencies
being pulled into user's lockfiles
2024-10-05 12:09:24 -07:00
seth e6203313ce flake: format with nixfmt 2024-10-05 12:09:24 -07:00
seth 938061dd5e flake: use nixfmt 2024-10-05 12:09:24 -07:00
Ivan Molodetskikh 0cca7a2116 default-config: Add more comments to prefer-no-csd 2024-10-01 13:28:28 +03:00
Ivan Molodetskikh 39b46b3326 default-config: Add rounded corner window rule example 2024-10-01 13:28:16 +03:00
Ivan Molodetskikh 2aebd6bdbb default-config: Add comments to consume/expel binds 2024-10-01 13:20:38 +03:00
Ivan Molodetskikh b501a9b303 Upgrade dependencies 2024-09-30 15:27:36 +03:00
Ivan Molodetskikh 94e5408f46 Update Smithay 2024-09-30 15:24:50 +03:00
Christian Meissl eb190e3f94 handle role specific buffer offset 2024-09-30 05:04:58 -07:00
spazzylemons 80bb0d5876 Remove one unnecessary .clone() call and reorder another 2024-09-30 00:45:44 -07:00
Marwin Kreuzig c04ccafd0a fix focus_up_or_right 2024-09-28 05:18:22 -07:00
sodiboo 6ee5b5afa7 flake: update inputs and remove crane.inputs.nixpkgs override
the input was removed in https://github.com/ipetkov/crane/pull/692
2024-09-15 08:05:05 -07:00
Ivan Molodetskikh 6a48728ffb Bump version to 0.1.9 2024-09-14 11:55:52 +03:00
Ivan Molodetskikh 9cb89ff26c wiki: Update default hotkeys list 2024-09-14 10:17:27 +03:00
Ivan Molodetskikh 4e5f392c50 wiki: Document always-center-focused-column 2024-09-14 09:48:59 +03:00
Ivan Molodetskikh e35d9e760b default-config: Uncomment BracketLeft/BracketRight
These are fairly useful.
2024-09-13 21:51:56 +03:00
Ivan Molodetskikh 22fee7b003 Add NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY env
Useful for UWSM I guess.
2024-09-13 15:45:30 +03:00
Ivan Molodetskikh e95d28e148 README: Remove NVIDIA note 2024-09-13 15:10:25 +03:00
Ivan Molodetskikh 7a65a0b79f wiki: Delete unstable JSON output note 2024-09-13 15:06:20 +03:00
Ivan Molodetskikh ca30315deb Set rust-version in Cargo.toml 2024-09-13 15:05:41 +03:00
Ivan Molodetskikh 9538e8f916 Upgrade dependencies 2024-09-13 15:05:33 +03:00
Ivan Molodetskikh 8b3715eabf Update Smithay 2024-09-13 14:59:32 +03:00
Ivan Molodetskikh d0f2b9abd0 Fix formatting 2024-09-12 20:54:44 +03:00
Ivan Molodetskikh 43578e21b1 Always clamp non-auto window height with >1 windows in column 2024-09-12 19:31:47 +03:00
Ivan Molodetskikh 55a798bd8b Prevent unintended focus-follows-mouse during workspace switch 2024-09-12 16:48:29 +03:00
Ivan Molodetskikh cdcd5a2835 Update comments 2024-09-12 13:36:08 +03:00
Ivan Molodetskikh 737e99ec69 Add preset window heights to wiki & default config 2024-09-12 02:32:44 -07:00
Ivan Molodetskikh c3cb42f04d Add SwitchPresetWindowHeight by id 2024-09-12 02:32:44 -07:00
Christian Rieger d0e624e615 Implement preset window heights 2024-09-12 02:32:44 -07:00
Ivan Molodetskikh 087a50a19c wiki/Xwayland: Add note about existing DISPLAY 2024-09-10 11:33:08 +03:00
Ivan Molodetskikh 0bed253835 tty: Try connecting with invalid modifier on fail 2024-09-10 11:12:24 +03:00
Ivan Molodetskikh 6b6a84e55b Avoid panics on more wrong VBlank events 2024-09-10 10:48:45 +03:00
Ivan Molodetskikh 7d5785e96f Give focus to on-demand layer surfaces on map 2024-09-10 10:14:34 +03:00
Ivan Molodetskikh 70fa38fadf Possibly fix some unsync subsurfaces not redrawing output 2024-09-10 09:52:31 +03:00
Ivan Molodetskikh 3514cd2e36 Prefer exclusive layer focus to on-demand on the same layer 2024-09-10 09:10:03 +03:00
Ivan Molodetskikh 96083847fb ipc: Clarify some things in the docs 2024-09-09 08:51:03 +03:00
Ivan Molodetskikh d25d6ce337 Arrange layer map after sending new scale/transform
I think that should be a slightly better ordering of events.
2024-09-08 22:33:09 +03:00
Ivan Molodetskikh bb044075fa Inform layer surfaces of scale/transform changes
How'd I miss this and then never catch it?
2024-09-08 22:05:56 +03:00
Ivan Molodetskikh 370fd4e172 ipc: Convert all Action unit variants to unit struct variants
This is a breaking change, but likely nobody uses this through raw JSON
yet, and this allows us to add fields to any action later on without
another breaking change.
2024-09-06 18:32:51 +03:00
Ivan Molodetskikh 7dea3822a3 Fix set-window-height SetProportion scale 2024-09-06 18:32:51 +03:00
Ivan Molodetskikh 7d11ef0abb Extract print_window() 2024-09-06 18:32:51 +03:00
Ivan Molodetskikh dcb29efce5 Implement by-id window addressing in IPC and CLI, fix move-column-to-workspace
This is a JSON-breaking change for the IPC actions that changed from
unit variants to struct variants. Unfortunately, I couldn't find a way
with serde to both preserve a single variant, and make it serialize to
the old value when the new field is None. I don't think anyone is using
these actions from JSON at the moment, so this breaking change is fine.
2024-09-06 18:32:41 +03:00
Ivan Molodetskikh cb5d97f600 Fix new Clippy warning
This was stabilized in 1.76 so we can use it now.
2024-09-05 20:40:11 +03:00
Ivan Molodetskikh 608ab7d8b1 Change output sorting to match make/model/serial first
We can do this now that we have libdisplay-info.
2024-09-05 20:10:01 +03:00
elkowar fd8ebb9d06 implement always_center_single_column layout option 2024-09-05 01:01:41 -07:00
Ivan Molodetskikh 952916fd1c layout: Prevent view gesture snap beyond first/last column 2024-09-04 21:46:08 +03:00
Ivan Molodetskikh a0592e8f53 layout: Extract snap_points() 2024-09-04 21:45:47 +03:00
Ivan Molodetskikh 5460c792bd Fix missing KeyboardLayoutSwitched event on XKB switch 2024-09-04 20:54:11 +03:00
sodiboo e5ecd27bbe flake: add libdisplay-info to buildInputs 2024-09-04 09:39:22 -07:00
Ivan Molodetskikh 4543873dae wiki/IPC: Link to the online rustdoc 2024-09-04 13:15:43 +03:00
Ivan Molodetskikh a2c855315c ci: Add niri-ipc rustdoc generation 2024-09-04 12:39:23 +03:00
Ivan Molodetskikh 6c4e4b374a ipc: Write some more docs 2024-09-04 12:29:26 +03:00
Ivan Molodetskikh 9ab887bec8 ipc: Don't re-export socket types 2024-09-04 12:03:13 +03:00
Ivan Molodetskikh 268591f343 wiki: Add Since note to other open-on-output properties 2024-09-03 14:36:23 +03:00
Ivan Molodetskikh a42717bcac wiki/Xwayland: Mention adding DISPLAY to config environment 2024-09-03 14:12:28 +03:00
Ivan Molodetskikh 6b013a08fc wiki: Update package list 2024-09-03 13:51:24 +03:00
Ivan Molodetskikh b65a243fc9 Remove warning about missing output config 2024-09-03 13:48:08 +03:00
Ivan Molodetskikh f0157e03e7 Use libdisplay-info for make/model/serial parsing, implement throughout 2024-09-03 13:48:08 +03:00
Ivan Molodetskikh 4b7c16b04a Read config from /etc/niri/config.kdl too 2024-09-02 13:10:45 +03:00
Ivan Molodetskikh aafd5ab70f wiki: Use $NIRI_SOCKET in example 2024-09-02 12:38:33 +03:00
Ivan Molodetskikh d8d6b5a5e0 wiki: Fix niri-ipc links 2024-09-02 10:05:59 +03:00
Ivan Molodetskikh a1fd4b396f wiki: Fix code block formatting 2024-09-02 10:04:43 +03:00
Ivan Molodetskikh 5521cdda63 wiki: Add the word IPC to the sidebar 2024-09-02 10:03:44 +03:00
Ivan Molodetskikh 12b16a9d7e wiki: Document IPC programmatic access 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh f7181fb066 Implement by-id workspace action addressing
It's not added to clap because there's no convenient mutually-exclusive
argument enum derive yet (to have either the current <REFERENCE> or an
--id <ID>). It's not added to config parsing because I don't see how it
could be useful there. As such, it's only accessible through raw IPC.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 17ac52e1d4 Fix spelling mistake 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 64a9351921 Add niri msg windows 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 332af8b062 Rearrange some CLI and IPC enum values 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh b7901579d5 Change IdCounter to be backed by an AtomicU64
Let's see if anyone complains.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 138c2a3bfd Change OutputId::get() to return u64 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 446a9f1e06 Make WorkspaceId inner field private 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 52265e2e19 utils/id: Use a Relaxed atomic op 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 0f522f209b Change MappedIt::get() to return u64 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 30b213601a Implement the event stream IPC 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 8eb34b2e18 Animate focus-workspace by idx/back and forth/previous
Deleting the test because it only made sense when no-animation was
special cased.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 74d1b1f406 layout: Cache monitor output name 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 2b3d196876 Remove unused function 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 397b7e4bb9 ipc: Read only a single line on the client
Allow extensibility.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 598b27f83c flake: Remove maintainer comment
Effectively other contributors maintain it now.
2024-08-26 18:15:39 +03:00
Ivan Molodetskikh da53e79d07 wiki: Add hotkey overlay skip to FAQ 2024-08-26 10:35:00 +03:00
Ivan Molodetskikh 2907d5af3e wiki: Mark FAQ snippet as KDL 2024-08-26 10:35:00 +03:00
sodiboo dd919fe01b fix cargo run on nixos
this boils down to adding some extra dependencies to the shell
environment. they're also inherited from craneArgs because the ones from
the package are actually transformed into the WRONG outputs of the
packages. also refactors to use craneLib.devShell because it's somewhat
cleaner.
2024-08-25 15:42:25 +03:00
Ivan Molodetskikh f86a9bed1a layout: Break out early on min size 2024-08-25 11:46:04 +03:00
Ivan Molodetskikh cfa87d508e layout: Fix rounding in height distribution
Rounding before checking min height could artificially increase the
window height that we check, leading to an incorrectly satisfied min
constraint.
2024-08-25 10:16:37 +03:00
Ivan Molodetskikh f19e1711a7 Add niri msg keyboard-layouts 2024-08-25 09:38:45 +03:00
Ivan Molodetskikh 20cd4f5d04 layout: Clamp window height to max available in column
When the window is alone in its column this logic intentionally isn't
triggered. Until we have a floating layer, there's no other way to get a
window larger than the screen, which I need.
2024-08-25 08:46:34 +03:00
Ivan Molodetskikh b2c7d3ad40 Rework PW screencast frame timing
- Remove the 0.5 ms hack.
- Add redraw scheduling to fix stuck frame if the last redrawn frame
  happened too soon.
2024-08-24 10:49:32 +03:00
Ivan Molodetskikh 4832924483 Update Smithay (layer-shell popup fix) 2024-08-24 07:22:57 +03:00
Ivan Molodetskikh 28a8a9ace2 Register deadline timer for closing transaction 2024-08-23 19:09:18 +03:00
Ivan Molodetskikh a4f1caab1d wiki: Update transaction list 2024-08-23 15:53:01 +03:00
Ivan Molodetskikh c8839f7658 Implement window close transaction
Mainly visible with disabled animations.
2024-08-23 15:41:06 +03:00
Ivan Molodetskikh dfe3580607 animation: Use saturating_sub in value() 2024-08-23 15:39:57 +03:00
Ivan Molodetskikh 1c02552e92 animation: Make restarted() take by-ref 2024-08-23 15:39:45 +03:00
Ivan Molodetskikh ff7cbb97df Fix screen transition across scale/transform changes 2024-08-23 12:54:07 +03:00
Ivan Molodetskikh 09f3d3fb12 Extract Niri::update_render_elements() 2024-08-23 12:54:07 +03:00
Ivan Molodetskikh 63defc25d2 Fix Clippy warnings 2024-08-23 12:21:47 +03:00
Ivan Molodetskikh db39fc95f4 pw_utils: Re-create damage tracker on scale change 2024-08-23 11:14:24 +03:00
Ivan Molodetskikh 471dc714aa Add damage check to PW screencasts
Avoids unnecessary frames.
2024-08-23 11:02:34 +03:00
Ivan Molodetskikh fef665df73 tty: Wait for sync on needs_sync()
How did I never add this back?
2024-08-23 09:26:42 +03:00
Ivan Molodetskikh 7bfdf87bf0 Implement resize transactions 2024-08-22 15:19:11 +03:00
Ivan Molodetskikh cf357d7058 Implement window resize throttling 2024-08-22 14:40:40 +03:00
Ivan Molodetskikh 618fa08aa5 Update Smithay (apply state in post commit) 2024-08-22 14:15:04 +03:00
Ivan Molodetskikh a40e7b4470 Handle dmabuf blocker separately in toplevel pre-commit
Will be needed for transactions.
2024-08-22 13:13:28 +03:00
Michael Yang f1894f6f9a feature: add on-demand vrr (#586)
* feature: add on-demand vrr

* Don't require connector::Info in try_to_set_vrr

* Improve VRR help message

* Rename connector_handle => connector

* Fix tracy span name

* Move on demand vrr flag set higher

* wiki: Mention on-demand VRR

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-08-22 11:58:07 +03:00
Ivan Molodetskikh dfc2d452c5 layout: Do not recompute total_weight every iteration 2024-08-15 11:46:13 +03:00
Ivan Molodetskikh 66f23c3980 layout: Implement weighted height distribution
The intention is to make columns add up to the working area height most
of the time, while still preserving the ability to have one fixed-height
window.

Automatic heights are now distributed according to their weight, rather
than evenly. This is similar to flex-grow in CSS or fraction in Typst.

Resizing one window in a column still makes that window fixed, however
it changes all other windows to automatic height, computing their
weights in such a way as to preserve their apparent heights.
2024-08-15 10:50:38 +03:00
Ivan Molodetskikh 7a6ab31ad7 layout: Pre-subtract gaps during height distribution
Same result, but code a bit clearer.
2024-08-15 10:46:39 +03:00
Ivan Molodetskikh 2f73dd5b59 wiki: Use real em-dash 2024-08-14 18:33:43 +03:00
Ivan Molodetskikh c658424c9f wiki: Document invisible state 2024-08-14 18:32:50 +03:00
Ivan Molodetskikh bb58f2d162 wiki: Clarify named workspaces example 2024-08-14 18:18:05 +03:00
Fea f54297f242 flake: Update flake inputs 2024-08-14 10:49:54 +03:00
Fea b72d946062 Fix nix build 2024-08-14 10:49:54 +03:00
Ivan Molodetskikh 883763c172 Implement stub mutter-x11-interop
Allows xdp-gnome dialogs to work with X11 clients.

Fixes https://github.com/YaLTeR/niri/issues/594
2024-08-13 09:15:57 +03:00
Ivan Molodetskikh 9063a5dbdc spec: Add mesa-libEGL dependency
Closes https://github.com/YaLTeR/niri/issues/554
2024-08-10 14:55:56 +03:00
Ivan Molodetskikh 892e848985 Update README 2024-08-10 12:55:47 +03:00
Ivan Molodetskikh 0edb90bab2 README: Add similar projects 2024-08-10 12:55:38 +03:00
Ivan Molodetskikh 8f71f8958e Bump version to 0.1.8 2024-08-10 12:55:24 +03:00
Ivan Molodetskikh fcb97cfd5e Update dependencies (Smithay Xwayland Nvidia freeze fix) 2024-08-09 19:58:07 +03:00
Ivan Molodetskikh 2983eb3113 wiki: Bump xwl-satellite higher up 2024-08-08 15:26:06 +03:00
Ivan Molodetskikh a968b1abc0 Fix redundant cast after upgrading csscolorparser 2024-08-08 15:12:48 +03:00
Ivan Molodetskikh 47c964d6fb Upgrade dependencies 2024-08-08 15:06:55 +03:00
Michael Yang 22cb657ef1 fix: change precision to highp 2024-08-08 15:06:23 +03:00
Ivan Molodetskikh bb15d1e850 screencopy: Change integer to fractional scale
That *was* wrong after all.
2024-08-08 13:54:28 +03:00
Ivan Molodetskikh 47680e43c5 screencopy: Wait for SyncPoint before submitting 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 0f1e44aac6 screencopy: Fix transformed damage calculation 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 66aae91bca screencopy: Clarify the use of integer scale 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 07bd76e219 screencopy: Use monotonic time
This way it matches up with presentation-time.
2024-08-08 13:32:37 +03:00
Michael Yang b6a7b3e9e4 feat: update screencopy to version 3 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 1cf5cfce06 Bump MSRV to 1.77.0
New pipewire-rs requires it.
2024-08-06 18:17:43 +03:00
Ivan Molodetskikh 8ff90c4fc2 Implement PipeWire DMA-BUF modifier negotiation 2024-08-06 18:01:52 +03:00
Ivan Molodetskikh 908c8eb42a wiki: Use HTML dark/light image
Apparently GitHub Markdown is not supported on GitHub Wiki.
2024-08-01 18:26:17 +03:00
Ivan Molodetskikh 0078293d4c wiki: Document the redraw loop 2024-08-01 17:52:34 +03:00
Jeff Peeler 9728dbeeac add mod3 key binding support (#565)
* add support for iso_level5_shift modifier

* update Cargo.lock

bumps smithay to de94e8f59e202b605c35dfe1fef1857bad427e8c
2024-07-31 15:00:35 +00:00
Ivan Molodetskikh 324029ca3b Deal with Clippy warnings 2024-07-28 11:41:09 +03:00
Ivan Molodetskikh 73be5b2ba1 CI: Switch leftover action to dtolnay/rust-toolchain
Missed this I guess.
2024-07-28 11:04:02 +03:00
Ivan Molodetskikh af904d23ac tty: Add check for vblank on idle 2024-07-27 13:43:27 +03:00
Ivan Molodetskikh ad84fc1479 wiki: Fix em-dash 2024-07-27 10:14:06 +03:00
Ivan Molodetskikh d5a8074b53 Add profile-with-tracy-ondemand feature
Finally this can be added without disabling frames.

manual-lifetime is needed to avoid initializing Tracy for CLI commands,
since that is quite slow.
2024-07-27 09:51:44 +03:00
Ivan Molodetskikh c506fecc87 Upgrade dependencies 2024-07-27 09:28:40 +03:00
Ivan Molodetskikh d777810911 pw: Don't require LINEAR buffer
It's not needed and apparently doesn't work on NVIDIA together with the
rendering flag.
2024-07-26 16:06:33 +03:00
Ivan Molodetskikh bbdc07ee6c wiki: Document output background-color 2024-07-26 11:51:29 +03:00
Anant Sharma 689338f059 Add background color option for output 2024-07-26 11:51:29 +03:00
Ivan Molodetskikh eee770514f wiki: Mention nightly COPR 2024-07-22 13:49:43 +03:00
Ivan Molodetskikh 5a0bda7ec4 wiki: Document negative struts 2024-07-22 13:12:42 +03:00
Ivan Molodetskikh b454fd5d9e Add negative struts to tests 2024-07-22 13:12:42 +03:00
Salman Farooq 2a830ed498 feat: negative struts (to remove outer gaps) 2024-07-22 13:12:42 +03:00
Ivan Molodetskikh e98d1ec5a7 Add an rpkg spec template 2024-07-17 22:08:15 +03:00
Ivan Molodetskikh 3ace97660f Implement gradient color interpolation option (#548)
* Added the better color averaging code (tested & functional)

* rustfmt

* Make Color f32 0..1, clarify premul/unpremul

* Fix imports and test name

* Premultiply gradient colors matching CSS

* Fix indentation

* fixup

* Add gradient image

---------

Co-authored-by: K's Thinkpad <K.T.Kraft@protonmail.com>
2024-07-16 07:22:03 +00:00
Ivan Molodetskikh 0824737757 border: Fix reversed gradient at angle = 90 2024-07-13 19:02:04 +03:00
Ivan Molodetskikh 8fdea033bc Fix Clippy warnings 2024-07-13 07:48:07 +03:00
Ivan Molodetskikh 2e906fc5fa Add middle-emulation libinput flag 2024-07-13 07:34:22 +03:00
Tglman a5a34934df feat: add metadata for generate deb package with cargo deb 2024-07-12 16:58:30 +03:00
Ivan Molodetskikh 08a8a0f29a Update Cargo.lock 2024-07-12 10:44:02 +03:00
Oli Strik 519611c6c8 Add schemars::JsonSchema trait to ipc types (#536)
* feat: add schemars JsonSchema trait to ipc types

* niri-ipc: use feature-flag for deriving schemars::JsonSchema

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-12 05:21:52 +00:00
Winter a283c34dbb Add move-column-{left/right}-or-to-monitor-{left/right} (#528)
* feature added, move-column-left-or-monitor-left and move-column-right-or-monitor-right

* fixed stupid mistake

* yalter's fixes

* fixed names

* fixed a stupid mistake

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-10 04:52:48 +00:00
Ivan Molodetskikh f9fe86ee3e Restore VRR on TTY switch 2024-07-09 14:25:02 +04:00
Ivan Molodetskikh 2e67152941 Fix view offset anim restart on switching focus 2024-07-09 09:50:46 +04:00
Ivan Molodetskikh 22bfec7259 Add tolerance to view offset anim restart check
It was getting tripped by tiny differences.
2024-07-09 09:43:43 +04:00
Suyashtnt 1af9f9bd95 niri-config: update wiki parses test to test all codeblocks
This makes sure the failing codeblocks do fail. This also optimizes the algorithm a bit by removing a `.collect()`

Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 19:43:07 +03:00
Suyashtnt 926451c8be wiki: update no-test comments in wiki
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 19:43:07 +03:00
Suyashtnt 7b3bef124d niri-config: add test to see if all snippets inside of the wiki compile
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Suyashtnt 3be6e38af3 wiki: update wiki kdl snippets
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Suyashtnt f2290a43d9 flake: update nix flake
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Ivan Molodetskikh 4513663084 screenshot-ui: Animate opening 2024-07-08 11:24:08 +04:00
Ivan Molodetskikh 092cf6cfaf solid_color: Fix alpha handling
It wasn't getting redrawn on alpha changes.
2024-07-08 11:11:06 +04:00
Ivan Molodetskikh 236f96e676 screenshot-ui: Add a help panel 2024-07-08 10:54:21 +04:00
Ivan Molodetskikh 887ca971ab Use is_alive() 2024-07-08 10:06:06 +04:00
Ivan Molodetskikh 4cc195b681 screenshot-ui: Pre-compute PrimaryGpuTexture 2024-07-08 10:04:43 +04:00
Ivan Molodetskikh fc2be2b8d0 Upgrade dependencies 2024-07-08 09:38:18 +04:00
Christian Meissl 570bf1cb3c bump smithay 2024-07-08 08:30:00 +03:00
Ivan Molodetskikh 6ec9c72539 Clear pointer grab upon opening the screenshot UI
Gets rid of DND surfaces.
2024-07-07 09:54:19 +04:00
Ivan Molodetskikh 1a1086206c Extract capture_screenshots() 2024-07-07 09:48:19 +04:00
Ivan Molodetskikh f2766b103d Implement toggling pointer for the screenshot UI 2024-07-07 09:23:59 +04:00
Ivan Molodetskikh 62c9d44b04 screenshot-ui: Fix last selection preservation
Another missed thing from the fractional scale refactor...
2024-07-07 09:22:39 +04:00
Ivan Molodetskikh e394a7ff20 Implement on-demand layer-shell keyboard focus 2024-07-06 18:20:19 +04:00
Ivan Molodetskikh 921ed63204 Add LayerSurface to PointerFocus 2024-07-06 18:17:48 +04:00
Ivan Molodetskikh 77dafb819f Fix screenshot UI selection pointer clamping 2024-07-06 09:46:37 +04:00
Ivan Molodetskikh 1da99f4003 Implement focus-follows-mouse max-scroll-amount 2024-07-05 20:53:11 +04:00
Ivan Molodetskikh 120eaa6c56 wiki: Fix repeat since annotation 2024-07-05 20:30:27 +04:00
Ivan Molodetskikh fb636ef98d Refactor and simplify new view offset calculation
* Split new offset computation from starting the animation.
* Simplify new column on empty workspace logic.
2024-07-05 20:30:27 +04:00
Ivan Molodetskikh 6147a31b48 wiki: Add Since to repeat=false 2024-07-05 12:04:23 +04:00
Ivan Molodetskikh 3f8707496f layout: Remove todo!() when activating window with no monitors 2024-07-05 11:56:45 +04:00
Ivan Molodetskikh de6caec685 Recompute current pointer focus for focus-follows-mouse
Fixes https://github.com/YaLTeR/niri/issues/377.
2024-07-05 10:13:50 +04:00
Ivan Molodetskikh c8411e55d9 wiki: Mention bind key repeat 2024-07-05 08:40:25 +03:00
Salman Farooq d3aebdbec4 Implement key repeat for compositor binds 2024-07-05 08:40:25 +03:00
TheAngusMcFire a56e4ff436 Added Commnads to focus windows or Monitors above/below the active window (#497)
* Implement focus-window-up/down-or-monitor calls

* Fixed wrong naming of focus-window-or-monitor commands

* fix copy pase errors for focusing direction

* Fixed wrong behaviour when the current workspace is empty

* Cleanup navigation code to reduce complexity

* Fix wrong comments and add testcases for FocusWindowOrMonitorUp/Down

---------

Co-authored-by: Christian Rieger <christian.rieger@student.tugraz.at>
2024-07-05 04:55:04 +00:00
Ivan Molodetskikh 9dcc9160b3 Put Outputs config into a dedicated struct 2024-07-05 07:35:01 +03:00
tet 43df7fad46 Implement wlr-output-management protocol
fix: wlr_output_management use WeakOutput
2024-07-05 07:35:01 +03:00
Ivan Molodetskikh d2087a2cd9 Add output ID tracking 2024-07-05 07:35:01 +03:00
Nick Hastings c681198179 Add install location instructions for manual installation (#489)
* wiki: Update install location instructions

Provide file install destinations for both packages and manual
installations.

* wiki: split install instructions into two sections

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-02 08:30:39 +00:00
it-a-me 105938df0b Keep monitors powered off upon connecting a new one (#488)
* Keep monitors powered off upon connecting a new one

Update src/backend/tty.rs

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

Update src/backend/tty.rs

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

fix tests

* Update

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-02 01:21:07 -07:00
Ivan Molodetskikh 7b6fa12854 Enable subpixel glyph positioning in Pango
Makes things scale more smoothly.
2024-07-01 09:47:31 +04:00
Ivan Molodetskikh e7c201abba Update README 2024-06-29 10:27:38 +04:00
Ivan Molodetskikh 4fd04951e6 Bump version to 0.1.7 2024-06-29 08:39:13 +04:00
Salman Farooq 747c186293 add-in-wiki-xwayland-run-as-a-solution-to-run-X-apps (#477) 2024-06-28 21:18:29 -07:00
Filipe Paniguel bdf9894020 feat: add focus-column-or-monitor-left, focus-column-or-monitor-right (#456)
* feat: add support for focus-window-or-monitor

* addresses output without window case

* refactor: reduce verbosity

* update this..

* refactor: rename `maybe_focus_window` functions

* refactor: flip focus_window_or_output return logic

* Update src/layout/mod.rs

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

* refactor: rename to Column

* move blocks next to other Column variables

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-28 07:44:24 -07:00
sodiboo d180e60e05 Implement support for $NIRI_CONFIG environment variable 2024-06-28 14:00:26 +03:00
sodiboo 65addefd09 wiki: Fix $XDG_CONFIG_HOME/.config/ that should be $XDG_CONFIG_HOME/ 2024-06-28 14:00:26 +03:00
Ivan Molodetskikh 697fcbac12 wiki: Add rounded corners to the FAQ 2024-06-28 14:39:04 +04:00
Ivan Molodetskikh a8e281e95f wiki: Fix links 2024-06-28 14:38:58 +04:00
Ivan Molodetskikh 4d60eae82e Fix blocked-out + popups and rounded corners window screencasts 2024-06-28 12:35:12 +04:00
Ivan Molodetskikh 2b5215c244 Show ISO_Level3_Shift in the hotkey overlay 2024-06-28 11:28:40 +04:00
Ivan Molodetskikh a43f30b7f5 Ignore compositor opacity for window screencasts
When using opacity as unfocused indicator, it will show up on the
screencast, which is undesired.

This is not a problem for window screen*shot*s where the window is
focused.
2024-06-28 10:39:36 +04:00
Ivan Molodetskikh 88f7b08e56 Add transparency support to window screencasts
Turns out it needed to be in a separate pod.
2024-06-28 10:39:35 +04:00
Ivan Molodetskikh dc92d80b9f Implement initial window screencasting 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 0757ad08e7 id: Start from 1 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 5577021475 wiki: Mention wait for completion NVIDIA flickering workaround 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 40aff3a094 Implement org/gnome/shell/Introspect/GetWindows 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 6c5f10035a mapped: Add id 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 96d2baa2b5 mapped: Make is_active_in_column private 2024-06-28 10:39:35 +04:00
aspizu 5d2754f831 Fix dead links and add FAQ entry (#475)
* Fix dead links and add FAQ entry

* Update wiki/FAQ.md

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

* Update wiki/Important-Software.md

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-27 23:23:52 -07:00
itsjunetime ebaf1b0620 Update winit to fix failing build on arm linux 2024-06-22 18:21:15 +03:00
Ivan Molodetskikh 589e5a600c Keep screencast running through size changes 2024-06-21 11:05:28 +03:00
Ivan Molodetskikh 198b5a502d Update dependencies 2024-06-21 08:55:46 +03:00
Ivan Molodetskikh cb0ebd35ce Make tablet without specific output map to union of outputs 2024-06-19 23:02:45 +03:00
Ivan Molodetskikh 29cf80a3dd wiki: Mention workspace switch mouse gesture 2024-06-19 22:22:34 +03:00
Ivan Molodetskikh db89d4d3dd Implement vertical middle mouse gesture 2024-06-19 21:55:39 +03:00
Kirill Chibisov 226273f660 Handle KDE decorations in Mapped::has_ssd
This fixes an issue with default CSD border being drawn for SSD
rendering firefox, because only xdg decorations were checked.
2024-06-19 17:42:29 +03:00
Ivan Molodetskikh c0ded35783 Somewhat fix height distribution logic
This got a bit broken with fractional layout. The current logic seems to
give exact results for integer scales again, but for fractional scales
sometimes the resulting height goes beyond the maximum, even clearly by
more than one logical pixel. Not entirely sure why that is.
2024-06-19 08:51:19 +03:00
FreeFull 39632e9c1e Add regex syntax link to Configuration:-Window-Rules.md 2024-06-18 14:31:57 +03:00
Ivan Molodetskikh 66202992c9 Fix blurry rounded corners on high scales 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh eb59b10050 config: Remove obsolete FIXME 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 986f2c14ab Make scale use FloatOrInt 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 793e1bdbc5 Animate xdg-activation and foreign-toplevel workspace switches
These are a bit jarring without an animation.
2024-06-18 14:01:34 +03:00
Ivan Molodetskikh d62721d5f8 Queue redraw after activation in xdg-activation 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh d54619e1d1 Remove unnecessary return 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 8425493ef5 Allow scale below 1 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 6121e64338 Add fractional scales to auto scale guessing 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 33b5beaeee Round scale to closest representable 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 1dae45c58d Refactor layout to fractional-logical
Lets borders, gaps, and everything else stay pixel-perfect even with
fractional scale. Allows setting fractional border widths, gaps,
struts.

See the new wiki .md for more details.
2024-06-18 14:01:28 +03:00
Ivan Molodetskikh 997119c443 Enable fractional scaling 2024-06-18 12:23:50 +03:00
Ivan Molodetskikh 032589446a Fix cached data not updating on config change 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 9ae98e09cb Update Smithay 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 2ffa1ae705 layout: Cache scale and transform on the workspace 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh fee72b87cf niri-config: Add pretty-assertions to tests
The config parse test is pretty big and it's impossible to tell the
difference from the normal assert.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 6c47bd6e80 Rename apply_scale to to_physical_precise_round
Consistency with Smithay.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 02c2972e74 ui/config_error_notification: Store TextureBuffers
Avoids re-importing every frame.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 4b830ee7ff ui/screenshot_ui: Correct fractional scaled behavior 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 8e41568ffd Add SolidColor{Buffer,RenderElement} 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh dbe810d3d8 Move apply_scale() to utils 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh a1563b9132 ui/config_error_notification: Make fractional-scaling aware 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 98aea9579f ui/exit_confirm_dialog: Make fractional-scaling aware 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 7019172b67 Add MemoryBuffer 2024-06-10 18:08:00 +03:00
Ivan Molodetskikh be62bd123a ui/hotkey_overlay: Make fractional-scaling aware 2024-06-10 18:08:00 +03:00
Ivan Molodetskikh 3c63be6261 Implement our own TextureBuffer/RenderElement
Supports fractional texture scale + has some getters.
2024-06-10 18:08:00 +03:00
Ivan Molodetskikh e3406ac255 Signal fractional scale to clients
Doesn't do anything yet because we don't bind the fractional scale
manager and don't allow fractional scales.
2024-06-10 18:08:00 +03:00
Ivan Molodetskikh 22a948cc75 Update dependencies 2024-06-10 18:07:51 +03:00
Peter Collingbourne bc3d6cac80 Implement xdg_activation_v1
Fixes #30.
2024-06-10 18:06:34 +03:00
James Sully a55e385b12 Add focus-column-right-or-first, focus-column-left-or-last (#391)
* add focus-column-right-or-first

* add focus-column-left-or-last
2024-06-09 11:14:51 +00:00
Ujp8LfXBJ6wCPR af6d84a7f8 Fix typos (#429)
* Fix typos reported by "typos" crate

https://github.com/crate-ci/typos

* Ignore typo datas -> data

See https://github.com/crate-ci/typos?tab=readme-ov-file#false-positives
for more configureability.

---------

Co-authored-by: Carl Hjerpe <git@hjerpe.xyz>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-09 10:50:22 +00:00
Ivan Molodetskikh f203c8729a Use generic Atomic for rlim_t
rlim_t is different between platforms.
2024-06-09 08:48:36 +03:00
galister dbf0dddfcc PointerMotionAbsolute: use union rect of all outputs 2024-06-07 19:50:48 +03:00
Ivan Molodetskikh c6c17cccac Add missing fullscreen check
Fixes crash when a window in a column requests to be unfullscreened.
2024-06-04 19:46:19 +03:00
Ivan Molodetskikh b5ad0e12fd Preserve empty named workspaces upon output removal
Not sure how we missed this.
2024-06-02 08:21:19 +03:00
Yuya Nishihara c8e46b9d17 Add "off" and "disabled-on-external-mouse" properties to input devices
This is called "events <mode>" in Sway, but we decided to use more abstracted
form for consistency with the other config items. "disabled-on-external-mouse"
is added only to touchpads, but there might be other devices that support this
option.

I think "off" also applies to keyboards, but I'm not going to add the one
because we don't have libinput machinery for the keyboard config, and it's
unlikely that user wants to disable _all_ keyboards. OTOH, pointer devices can
be disabled per type. Perhaps, this should be revisited after implementing #371.
2024-05-29 16:41:03 +03:00
Yuya Nishihara f2ce84b243 Fix copy-paste error in scroll-method error message 2024-05-28 15:35:45 +03:00
rustysec ae7fb4c4f4 Add niri msg focused-output 2024-05-26 21:29:22 +04:00
Yuya Nishihara 4746a0da7d Add scroll-method property to pointer devices
My use case is to enable middle-button scroll on my keyboard with pointing
stick. The device is recognized as USB mouse.
2024-05-26 16:49:40 +03:00
Ivan Molodetskikh 2ac8d84034 Update Smithay (NVIDIA 555 fix) 2024-05-24 16:47:55 +04:00
Micah N Gorrell eb0f7aa429 Added actions to allow focusing up or down as normal but to wrap to the column to the left or right if there is no window above or below 2024-05-24 16:44:20 +04:00
Ivan Molodetskikh bcca03cce7 Increase RLIMIT_NOFILE to maximum
Fixes Xwayland + RustRover crashing.

See similar changes:
* https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2235
* https://github.com/swaywm/sway/pull/6629
2024-05-23 09:59:34 +04:00
Ivan Molodetskikh efb39e466b default-config: Clarify spawn comments 2024-05-21 22:33:50 +04:00
Ivan Molodetskikh 14d637f4ef wiki: Mention left-handed 2024-05-21 11:06:52 +04:00
Ivan Molodetskikh c9d90afe59 Add left-handed input property
Closes https://github.com/YaLTeR/niri/issues/366
2024-05-21 10:10:11 +04:00
Ivan Molodetskikh d088ce248f wiki: Mention xwayland-satellite 2024-05-21 08:17:57 +04:00
Ivan Molodetskikh f4cdde1f4f Fix no outputs case handling in a few places 2024-05-20 15:36:08 +04:00
Ivan Molodetskikh 56e02a398d Add Default impl for niri_config::Keyboard
Fixes https://github.com/YaLTeR/niri/issues/357
2024-05-19 17:55:54 +04:00
lpnh 2552b129c4 refactor: make example ready to copy and paste 2024-05-18 20:17:39 +03:00
Ivan Molodetskikh d96a66ddff Update README 2024-05-18 15:00:39 +04:00
Ivan Molodetskikh bfaf9ae060 Bump version to 0.1.6 2024-05-18 14:35:42 +04:00
Ivan Molodetskikh 2da0aaace8 wiki: Update different-corner-radius image 2024-05-18 10:50:11 +04:00
Ivan Molodetskikh ee12bbc9ed wiki: Change two instances of Telegram to Fractal 2024-05-18 09:44:48 +04:00
Ivan Molodetskikh cc4026f588 wiki: Add since to interactive resize 2024-05-18 08:59:11 +04:00
Ivan Molodetskikh aa74120143 wiki: Fix typo 2024-05-18 08:50:17 +04:00
Ivan Molodetskikh 473ef22de2 Redraw on lock surface children commits 2024-05-17 15:59:49 +04:00
Ivan Molodetskikh d76b213e03 Update Smithay (session-lock fix) 2024-05-17 15:49:02 +04:00
Ivan Molodetskikh 4dc7a6ceb8 Rearrange CLI subcommands a bit 2024-05-17 10:33:00 +03:00
rustysec 36d3e70f11 Implement niri msg workspaces 2024-05-17 10:33:00 +03:00
Ivan Molodetskikh a2f74c9bff Update Smithay (buffer leak and crash fix) 2024-05-17 07:54:56 +04:00
Ivan Molodetskikh 0ce08b598c Bump package versions 2024-05-16 18:04:18 +04:00
Ivan Molodetskikh ae63773737 Update Smithay and other deps 2024-05-16 18:00:28 +04:00
Ivan Molodetskikh c5ca412829 wiki: Add since to named workspaces 2024-05-16 13:04:51 +04:00
Ivan Molodetskikh cbfc682f9a Implement at-startup window rule 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh c64d9e5223 Fix missing check in Match PartialEq 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh 4e31f7e047 wiki: Document named workspaces 2024-05-16 01:24:34 -07:00
Ivan Molodetskikh 109d99fe82 Make workspace names case-insensitive 2024-05-16 01:24:34 -07:00
Gergely Nagy eb9bbe3352 Implement named workspaces
This is an implementation of named, pre-declared workspaces. With this
implementation, workspaces can be declared in the configuration file by
name:

```
workspace "name" {
  open-on-output "winit"
}
```

The `open-on-output` property is optional, and can be skipped, in which
case the workspace will open on the primary output.

All actions that were able to target a workspace by index can now target
them by either an index, or a name. In case of the command line, where
we do not have types available, this means that workspace names that
also pass as `u8` cannot be switched to by name, only by index.

Unlike dynamic workspaces, named workspaces do not close when they are
empty, they remain static. Like dynamic workspaces, named workspaces are
bound to a particular output. Switching to a named workspace, or moving
a window or column to one will also switch to, or move the thing in
question to the output of the workspace.

When reloading the configuration, newly added named workspaces will be
created, and removed ones will lose their name. If any such orphaned
workspace was empty, they will be removed. If they weren't, they'll
remain as a dynamic workspace, without a name. Re-declaring a workspace
with the same name later will create a new one.

Additionally, this also implements a `open-on-workspace "<name>"` window
rule. Matching windows will open on the given workspace (or the current
one, if the named workspace does not exist).

Signed-off-by: Gergely Nagy <niri@gergo.csillger.hu>
2024-05-16 01:24:34 -07:00
Ivan Molodetskikh 229ca90507 wiki: Mention where to find shader compile warnings 2024-05-15 22:15:39 +04:00
Ivan Molodetskikh 17a71bd424 wiki: Add expanding circle example to window-open 2024-05-15 21:24:18 +04:00
Ivan Molodetskikh a39aaa312d wiki: Add fall_and_rotate window-close custom shader example 2024-05-15 20:55:16 +04:00
Ivan Molodetskikh 3f802d0193 Clarify surface destroyed comment 2024-05-15 20:30:02 +04:00
Ivan Molodetskikh df36eac25b Fix render elements looking off on screenshots 2024-05-15 20:09:49 +04:00
Ivan Molodetskikh 609b1a02d0 Change resize shader geo size to logical pixels
Consistent with the others.
2024-05-15 19:52:11 +04:00
Ivan Molodetskikh 5335ef454b Implement custom shader for window-open 2024-05-15 19:38:29 +04:00
Ivan Molodetskikh 496cd59df9 Use correct function name in comment 2024-05-15 16:51:43 +04:00
Ivan Molodetskikh 3e385d5c48 Clear fd flags before sending selection 2024-05-15 16:49:46 +04:00
Ivan Molodetskikh b87fba2182 tty: Relax device checks on removal 2024-05-15 08:14:09 +04:00
Ivan Molodetskikh 3d63f5e644 tty: Try harder to find a GBM device 2024-05-15 08:13:56 +04:00
Ivan Molodetskikh 1096f0cf0e wiki: Mention kmsro in getting started 2024-05-15 00:30:20 +04:00
Ivan Molodetskikh 78978219a0 tty: Relax primary render node check 2024-05-14 23:39:22 +04:00
Ivan Molodetskikh 5999ba6a5e Avoid changing the view offset if size didn't change 2024-05-14 23:39:19 +04:00
Ivan Molodetskikh 94a9b48a0f Improve interactive resize end edge cases and animations 2024-05-14 20:41:10 +04:00
Ivan Molodetskikh d776ab7763 Fix interactive resize cancelling
The interactive resize may have ended, but we're still waiting for the
last commit of the respective window. When cancelling, we should cancel
those ones too.
2024-05-14 16:29:03 +04:00
Ivan Molodetskikh 5f40221051 Refactor column and tile offsets, fix a few issues 2024-05-14 15:35:43 +04:00
Ivan Molodetskikh b14405904a Draw closing windows in the right order 2024-05-14 14:52:13 +04:00
Ivan Molodetskikh e06776c5d4 wiki: Expand design principles a bit 2024-05-13 08:35:19 +04:00
Ivan Molodetskikh 55e550262d wiki: Fix custom shader examples 2024-05-12 10:08:06 +04:00
Ivan Molodetskikh e5ccc9332c wiki: Fix shader example links 2024-05-12 10:06:26 +04:00
Ivan Molodetskikh 36a54615ca Add crossfade_or_crop_next resize shader example 2024-05-12 09:56:11 +04:00
Ivan Molodetskikh 9004c83954 Implement custom shader for window-close anim 2024-05-12 09:52:36 +04:00
Ivan Molodetskikh 29c7552852 Add linear animation curve 2024-05-12 09:50:16 +04:00
Ivan Molodetskikh d2ed42a157 closing_window: Pass geo size and view rect 2024-05-12 08:46:02 +04:00
Ivan Molodetskikh 4073f9f522 closing_window: Remove starting_alpha/scale 2024-05-12 08:42:43 +04:00
Ivan Molodetskikh 464441f9eb closing_window: Store textures directly 2024-05-11 17:54:27 +04:00
Ivan Molodetskikh bc29256b9d Implement Mod+MMB view offset gesture 2024-05-11 14:02:37 +04:00
Ivan Molodetskikh beba87354a Group input-related things in a subfolder 2024-05-11 13:21:05 +04:00
Ivan Molodetskikh 078724369d wiki: List debug key binds 2024-05-11 12:56:34 +04:00
Ivan Molodetskikh 75393faca3 wiki: Add a few missing things 2024-05-11 12:55:18 +04:00
Ivan Molodetskikh 22cdd044d3 Reset double click timer on gesture trigger 2024-05-11 11:21:57 +04:00
Ivan Molodetskikh 719270854a Update resize commit unconditionally 2024-05-11 10:59:46 +04:00
Ivan Molodetskikh 8900960e76 Don't pass double-resize-right click to window 2024-05-11 10:52:21 +04:00
TheZoq2 47a8e75fd5 Add is_active_in_column
Add missing ```

Fix tests
2024-05-11 10:42:49 +04:00
Ivan Molodetskikh 6d9cfe2882 Don't start a resize if edges is empty 2024-05-11 10:30:51 +04:00
Ivan Molodetskikh de0ad85711 Set cursor for niri-initiated interactive resize 2024-05-11 10:28:38 +04:00
Ivan Molodetskikh f091e64b12 wiki: Add gestures page 2024-05-11 10:09:49 +04:00
Ivan Molodetskikh e454cd6282 Implement double-resize-click to reset height/toggle full width 2024-05-11 10:02:48 +04:00
Ivan Molodetskikh 1c14a0a2a9 Add a reset-window-height action 2024-05-11 09:33:23 +04:00
Ivan Molodetskikh 2fd9a03bd7 Stop confining the pointer during resize grab 2024-05-11 09:26:49 +04:00
Ivan Molodetskikh b101f9b5f8 Render tiles flush to the right when left-resizing
This really needs a refactor...
2024-05-11 09:00:03 +04:00
Ivan Molodetskikh 34bcc6ea93 Split get resize data from update 2024-05-11 08:26:49 +04:00
Ivan Molodetskikh 9dfa121b8e Implement interactive mouse resizing 2024-05-10 20:23:08 +04:00
Ivan Molodetskikh c4ebb9f58e Start Tracy manual-lifetime after niri msg 2024-05-09 11:08:15 +04:00
Ivan Molodetskikh 38e329aab9 Make async-channel non-optional 2024-05-08 08:57:37 +04:00
Ivan Molodetskikh 95a1a01fdc wiki: Add Since to do-screen-transition 2024-05-08 08:43:01 +04:00
Ivan Molodetskikh c61940c40e ipc: Wait until action is processed before returning 2024-05-08 08:30:49 +04:00
Ivan Molodetskikh ed2b6d3894 Mark screen transition texture transparent 2024-05-08 08:21:15 +04:00
Ivan Molodetskikh 47925948a3 Add trace span to do_screen_transition 2024-05-08 08:21:04 +04:00
Ivan Molodetskikh 5248e53499 Implement do-screen-transition action 2024-05-07 22:19:11 +04:00
Ivan Molodetskikh 9847a652af ipc: Respect --json for msg output 2024-05-05 13:08:29 +04:00
Ivan Molodetskikh 96823eea38 Make output name matching case-insensitive 2024-05-05 12:55:57 +04:00
Ivan Molodetskikh ea59091869 Print message when output was not found 2024-05-05 12:50:18 +04:00
Ivan Molodetskikh 2e4a2e13b1 Make missing scale = automatic selection
That was the intention, but I missed it before.
2024-05-05 12:39:20 +04:00
Ivan Molodetskikh df0ee996ee Don't unwrap client
If Smithay posts an error, client will become None immediately, even
while the surface may still receive events.
2024-05-05 11:14:46 +04:00
Ivan Molodetskikh 65b9c74f62 Implement niri msg output 2024-05-05 10:19:47 +04:00
Ivan Molodetskikh 2dff674470 Don't expand zero radius per corner
So that radii like 8 8 0 0 look properly.
2024-05-05 07:43:21 +04:00
Ivan Molodetskikh 23850e1c60 wiki: Try to fix link 2024-05-04 21:16:43 +04:00
Ivan Molodetskikh 641b44e006 Fix blocked-out surfaces on scaled outputs 2024-05-04 20:13:53 +04:00
Ivan Molodetskikh 1394afaae9 wiki: Mention nixos and nvidia issues in getting started 2024-05-04 16:27:14 +04:00
Ivan Molodetskikh 314ad9d3e5 Fix rounded corners on blocked-out resizes 2024-05-04 11:54:52 +04:00
Ivan Molodetskikh 99eb1227b1 Extract RenderTarget::should_block_out() 2024-05-04 11:51:27 +04:00
Ivan Molodetskikh 79093baeee Extract rules out 2024-05-04 11:45:39 +04:00
Ivan Molodetskikh 7093385b4d Update tile before taking unmap snapshot 2024-05-04 11:37:58 +04:00
Ivan Molodetskikh 3748f6cd6a Fix border/focus ring options not applying right away 2024-05-04 11:11:01 +04:00
Ivan Molodetskikh 73cc0079d6 Split update_render_elements() from advance_animations()
advance_animations() is called from places like input, whereas
update_render_elements() is strictly for rendering.
2024-05-04 11:10:02 +04:00
Ivan Molodetskikh 69aeba2a4d shader_element: Store and set location separately 2024-05-04 09:49:32 +04:00
Ivan Molodetskikh 7aab413048 shader_element: Remove size
It's not actually needed.
2024-05-04 09:15:17 +04:00
Ivan Molodetskikh 74996a2416 Make BorderRenderElement scale-agnostic 2024-05-03 21:49:47 +04:00
Ivan Molodetskikh 8ab50f9d1c shader_element: Store program type instead of shader 2024-05-03 21:23:32 +04:00
Ivan Molodetskikh 5c32031111 shader_element: Make shader optional
The element is long-lived, but the shader itself isn't.
2024-05-03 20:20:36 +04:00
Ivan Molodetskikh 85680a57da Reduce unnecessary damage to borders 2024-05-03 13:46:33 +04:00
Ivan Molodetskikh 1a8d6b1f1d Add a semi-working debug-toggle-damage binding 2024-05-03 10:33:31 +04:00
Ivan Molodetskikh 185f294200 wiki: Mention new debug option 2024-05-03 10:23:48 +04:00
Ivan Molodetskikh c6d64dae7a Add debug-toggle-opaque-regions 2024-05-02 17:52:06 +04:00
Ivan Molodetskikh 5dddc850fc wiki: Clarify getting started 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh 2f42f8ac75 Damage window on corner radius changes 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh 42cef79c69 Implement rounded window corners 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh d86df5025c Add Tracy span to Tile::render_inner 2024-05-01 19:04:47 +04:00
Ivan Molodetskikh 9309b3be61 Split rendering between popups and window surface 2024-05-01 19:04:11 +04:00
Ivan Molodetskikh c5be2dd549 Add Tracy span to Tile::render 2024-05-01 19:00:54 +04:00
Ivan Molodetskikh 365dbacae7 Move unmap snapshot from Mapped to Tile 2024-05-01 19:00:19 +04:00
Ivan Molodetskikh af9caa1d9b wiki: Warn against --all-features in getting started 2024-05-01 12:10:38 +04:00
Michael Forster 68ff36f683 Add libXcursor and libXi to nix flake
In my tests this was necessary to develop Niri using non-NixOS Nix.
Otherwise Niri panics with this error message: called `Result::unwrap()`
on an `Err` value: EventLoopCreation(NotSupported(NotSupportedError)).
2024-04-30 02:19:28 -07:00
Ivan Molodetskikh c0d5001e90 Update Smithay 2024-04-29 14:27:38 +04:00
Ivan Molodetskikh f3ded0c2e6 Move shader get out of ResizeRenderElement::new 2024-04-29 14:27:38 +04:00
Ivan Molodetskikh f43fa55526 Fix fullscreen backdrop rendering below focus ring 2024-04-28 06:48:48 +04:00
Ivan Molodetskikh c1c43c5393 Fix size_curr_geo in resize shader 2024-04-27 13:12:21 +04:00
Ivan Molodetskikh 5899010c96 Extract mat3_uniform 2024-04-27 13:11:25 +04:00
Ivan Molodetskikh 9f3715b731 Add distro to issue template 2024-04-27 07:44:51 +04:00
Ivan Molodetskikh 8d99e3c015 Add disable-direct-scanout debug flag 2024-04-25 22:10:52 +04:00
Ivan Molodetskikh 9df71bcb5d Add fixme comment 2024-04-25 08:48:35 +04:00
Ivan Molodetskikh 04c5b9ad74 Only give keyboard focus to exclusive layer-shell surfaces
Workaround until we properly support on-demand.

See: https://github.com/YaLTeR/niri/issues/308
2024-04-25 08:43:37 +04:00
Ivan Molodetskikh fd6c8c7790 Implement focus-ring window rule 2024-04-24 22:17:53 +04:00
Ivan Molodetskikh 3e598c565e Implement border window rule 2024-04-24 22:01:26 +04:00
Ivan Molodetskikh e261b641ed Filter out the Intel CCS modifiers 2024-04-24 12:26:59 +04:00
Ivan Molodetskikh dc1d2b706c Implement ideal scale factor guessing 2024-04-24 12:26:59 +04:00
Ivan Molodetskikh f9b008163c Fix spelling mistake 2024-04-23 00:09:42 -07:00
Kirill Chibisov 279659ac90 Unconstrain InputMethod's PopupSurface
Make IME popup to be visible inside the parent and not obscure the
text input rectangle region.

Fixes https://github.com/YaLTeR/niri/issues/221
2024-04-23 00:09:42 -07:00
Kirill Chibisov c2d03d82ce Use PopupKind instead of PopupSurface 2024-04-23 00:09:42 -07:00
Ivan Molodetskikh 5299590290 Improve cropping logic in resize shader example
The previous logic failed to the left of the geometry.
2024-04-22 22:37:47 +04:00
Ivan Molodetskikh 1681ed16d9 Change custom-shader to a prelude-epilogue system 2024-04-22 19:05:11 +04:00
Ivan Molodetskikh d4bed70884 Advertise Abgr8888 and Xbgr8888 in shm 2024-04-22 17:47:12 +04:00
Ivan Molodetskikh 49f5402669 Implement window-resize custom-shader 2024-04-21 20:16:54 +04:00
Ivan Molodetskikh 2ecbb3f6f8 Remove obsolete comment 2024-04-21 12:28:49 +04:00
Ivan Molodetskikh 6a80078259 README: Bring back NVIDIA issues note 2024-04-20 17:45:17 +04:00
Ivan Molodetskikh 303c51ee20 README: Update demo video 2024-04-20 17:30:35 +04:00
Ivan Molodetskikh 37a836f462 Bump version to 0.1.5 2024-04-20 16:55:39 +04:00
Ivan Molodetskikh 361ede4bcd wiki: Mention border background window rule in the FAQ 2024-04-20 16:52:51 +04:00
Ivan Molodetskikh 4fc80124ad Move info from README to Getting Started wiki page 2024-04-20 11:24:33 +04:00
Ivan Molodetskikh ba44aeda4a wiki: Add a FAQ page 2024-04-20 10:24:20 +04:00
sodiboo b5f7e4bd83 niri_ipc::Socket; niri msg version; version checking on IPC (#278)
* Implement version checking in IPC

implement version checking; streamed IPC

streamed IPC will allow multiple requests per connection

add nonsense request

change inline struct to json macro

only check version if request actually fails

fix usage of inspect_err (MSRV 1.72.0; stabilized 1.76.0)

"nonsense request" -> "return error"

oneshot connections

* Change some things around

* Unqualify niri_ipc::Transform

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-04-19 13:02:32 +00:00
Ivan Molodetskikh b98b95883d wiki: Attempt to fix broken tip 2024-04-19 14:47:06 +04:00
Ivan Molodetskikh 568c35ff87 Synchronize column removal anim on consume left/right
Visible when consuming left/right when always-centered and differing
horizontal view anim.
2024-04-19 13:48:39 +04:00
Ivan Molodetskikh c4f600bded wiki: Add missing newline 2024-04-19 12:49:11 +04:00
Ivan Molodetskikh 2c8d1030ab Separate tile X and Y movement animations
Helps with the jank caused by lack of transactions when consuming to the
left/right. Resize triggers a few frames later and restarts the
movement. Now it only restarts the vertical and not the horizontal
movement.
2024-04-19 12:44:24 +04:00
Ivan Molodetskikh f51dd67f2d wiki: Add Since to allow-when-locked 2024-04-19 11:29:01 +04:00
Ivan Molodetskikh 3509de6fbf default-config: Add mic mute bind 2024-04-19 11:14:51 +04:00
Ivan Molodetskikh 0477986a0d wiki: Move overdamped spring warning higher 2024-04-19 10:50:30 +04:00
Ivan Molodetskikh 914237fa11 Add allow-when-locked=true spawn bind property 2024-04-19 10:49:46 +04:00
Ivan Molodetskikh 0b93c46ce8 animation: Scale initial velocity by slowdown 2024-04-18 21:55:01 +04:00
Ivan Molodetskikh 0fcd981b86 Fix crop + crossfade artifacts 2024-04-18 21:39:27 +04:00
Ivan Molodetskikh 5c4153e26b wiki: Add a warning about overdamped springs 2024-04-18 20:51:25 +04:00
Ivan Molodetskikh 4d010b7943 animation: Clamp spring value
I've had an overdamped spring return an extreme value and trip up
an integer overflow check.
2024-04-18 20:45:37 +04:00
Ivan Molodetskikh 65c342f2cb config: Rearrange animations in struct 2024-04-18 17:36:12 +04:00
Ivan Molodetskikh 47f6c85f64 Preserve tile move config on animation restarts
This fixes a problem where consume-into-column would use resize
animation config instead of the window-movement config in most cases
(since a resize comes very shortly after the move starts).

A similar change to the column movement anim is more detrimental than
it's worth.
2024-04-18 00:30:12 +04:00
Ivan Molodetskikh 3b37f1a557 Sync expel animations 2024-04-17 18:03:17 +04:00
Ivan Molodetskikh dee0abb713 wiki: Clarify animations 2024-04-17 15:10:42 +04:00
Ivan Molodetskikh bbb4a64126 Use correct animation config for tile removal 2024-04-17 14:38:34 +04:00
Ivan Molodetskikh dfe49aa705 Use movement anim for view anim during movement 2024-04-17 14:29:22 +04:00
Ivan Molodetskikh 7ca39baf9e Add view anim functions with config argument 2024-04-17 14:23:47 +04:00
Ivan Molodetskikh 73e9ef5fe2 Resolve animation defaults during parsing 2024-04-17 14:06:32 +04:00
Ivan Molodetskikh c40d4f3268 Include resized window in left move 2024-04-17 10:35:46 +04:00
Ivan Molodetskikh 1b496ee21f Clamp animated window size 2024-04-16 17:44:06 +04:00
Ivan Molodetskikh bde46dab52 wiki: Mention consume/expel in window-movement anims 2024-04-16 11:02:21 +04:00
Ivan Molodetskikh 21ef5aded8 Remove jumps on consume/expel animation start 2024-04-16 10:48:54 +04:00
Ivan Molodetskikh b288102866 Implement consume/expel animations 2024-04-16 09:58:39 +04:00
Ivan Molodetskikh ff42f9b9d3 Start move animations from add/remove window/column 2024-04-16 08:59:15 +04:00
Ivan Molodetskikh c163e58167 Animate movement and resize on window closing in a column 2024-04-16 08:16:34 +04:00
Ivan Molodetskikh a9094b43d4 wiki: Mention niri msg outputs for VRR 2024-04-16 07:56:32 +04:00
Ivan Molodetskikh 9e33320b11 wiki: Clarify window-movement animation 2024-04-15 23:07:14 +04:00
Ivan Molodetskikh c40de5364d Add vrr_supported/enabled to output IPC 2024-04-15 22:29:25 +04:00
Ivan Molodetskikh 69f723d68a Implement vertical window move animations 2024-04-15 21:19:09 +04:00
Ivan Molodetskikh 568fbe26fe Avoid continuous redrawing during horizontal gesture 2024-04-14 14:29:41 +04:00
Ivan Molodetskikh f8412ecff3 wiki: Add since to VRR 2024-04-14 13:15:52 +04:00
Ivan Molodetskikh 3c6d8062c5 Add variable-refresh-rate flag 2024-04-14 09:37:42 +04:00
Ivan Molodetskikh 40374942db tty: Shorten non-desktop check 2024-04-14 08:08:09 +04:00
Ivan Molodetskikh 2c873044e8 Restore view offset upon unfullscreening 2024-04-13 20:07:37 +04:00
Ivan Molodetskikh 1336a581a6 tile: Fix returned snapshot size 2024-04-13 18:05:56 +04:00
Ivan Molodetskikh 8b0dc1902c Set window-resize animation config for view-offset anim caused by resize 2024-04-13 14:57:55 +04:00
Ivan Molodetskikh 9d5f1c7ef7 Unify Animation- and RenderSnapshot 2024-04-13 14:16:07 +04:00
Ivan Molodetskikh 71be19b234 Implement window resize animations 2024-04-13 11:07:23 +04:00
Ivan Molodetskikh 4fd9300bdb Fix typo 2024-04-13 10:58:32 +04:00
Ivan Molodetskikh 2bb6dd8c48 Move unmapped check to a pre-commit hook 2024-04-13 09:12:32 +04:00
Ivan Molodetskikh 7319f37f7a Add render_to_encompassing_texture() 2024-04-12 20:38:51 +04:00
Ivan Molodetskikh 0cd149c939 animation: Tweak clamped duration logic 2024-04-10 12:09:54 +04:00
Ivan Molodetskikh 5383a0591f Use clamped animations where it makes sense 2024-04-10 11:28:49 +04:00
Ivan Molodetskikh 0c68609063 animation: Implement clamped value and duration 2024-04-10 11:28:02 +04:00
Ivan Molodetskikh 6cd3f96a10 Fix building on stable 2024-04-10 09:26:56 +04:00
Ivan Molodetskikh 1888696567 Reimplement window closing anim in an efficient way
- Keep a root surface cache to be accessible in surface destroyed()
- Only snapshot during / right before closing, rather than every frame
- Store textures rather than elements to handle scale and alpha properly
2024-04-10 09:14:04 +04:00
Ivan Molodetskikh b9e789619f wiki: Fix wrong since annotation spot 2024-04-09 23:56:40 +04:00
Ivan Molodetskikh dd011f1012 Implement window closing animations 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 301a2c0661 layout: Fix view jumps when removing a window on the left 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 956bf7c0a8 Add missing mouse warp to commit unmap 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 209492e700 Add ease-out-quad curve 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 7e0d3d31f7 Update Smithay 2024-04-09 19:06:13 +04:00
Ivan Molodetskikh e448cfb0ef Adjust view offset anim together with offset
Not doing this caused quickly moving a column right and left to base the
final view position on an incorrect view offset.
2024-04-08 22:16:35 +04:00
Ivan Molodetskikh 6aceb3a798 Render active column in front
Rather than just the active window. This is visible on the new window
movement animations.
2024-04-08 19:48:52 +04:00
Ivan Molodetskikh 4856522a7a Implement window open shift in terms of window-movement
This removes the quite unobvious visual size, and fixes jerking when
opening multiple windows in quick succession.
2024-04-08 19:25:45 +04:00
Ivan Molodetskikh c1432bfa96 Implement column movement animation 2024-04-08 19:11:25 +04:00
Ivan Molodetskikh ec0531264e Avoid move_left() in expel-left 2024-04-08 19:11:25 +04:00
Ivan Molodetskikh 03fc439150 layout: Fix view_offset value when moving column 2024-04-08 17:34:39 +04:00
Ivan Molodetskikh 83aec41df3 Hide pointer on touch interaction 2024-04-06 10:57:12 -07:00
Ivan Molodetskikh 8be9381974 wiki: Add gamescope to the Xwayland page 2024-04-03 17:21:36 +04:00
Ivan Molodetskikh dc56f9885c wiki: Improve Xwayland page 2024-04-03 17:16:53 +04:00
Ivan Molodetskikh 2b3a80b477 wiki: Document IPC backwards compatibility 2024-04-02 09:08:36 +04:00
Ivan Molodetskikh 294f16f76c Fix typo in comment 2024-04-02 08:44:08 +04:00
Ivan Molodetskikh 4f56ff16f9 Fix and add missing calls to DRM leasing 2024-04-01 08:30:27 +04:00
Ivan Molodetskikh fe79a6a4e2 Clarify PipeWire error message 2024-03-31 11:36:04 +04:00
Ivan Molodetskikh 950fcf6328 Set SIGPIPE to SIG_DFL before printing in niri msg 2024-03-31 09:10:15 +04:00
3329 changed files with 83199 additions and 8485 deletions
+3
View File
@@ -14,6 +14,9 @@ assignees: ''
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
* Distro:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
+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"
+50 -31
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
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
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.72.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
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.72.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
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,25 +142,23 @@ jobs:
run: cargo clippy --all --all-targets
rustfmt:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install nightly --profile minimal --component rustfmt
rustup override set nightly
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Run rustfmt
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
@@ -174,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
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:
@@ -194,7 +188,7 @@ jobs:
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- run: nix build
- run: nix flake check
continue-on-error: true
publish-wiki:
@@ -202,10 +196,35 @@ jobs:
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
rustdoc:
needs: build
permissions:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: dtolnay/rust-toolchain@stable
- name: Generate documentation
run: cargo doc --no-deps -p niri-ipc
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
- name: Deploy documentation
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
force_orphan: true
+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
+1726 -1252
View File
File diff suppressed because it is too large Load Diff
+76 -38
View File
@@ -1,22 +1,29 @@
[workspace]
members = ["niri-visual-tests"]
members = [
"niri-config",
"niri-ipc",
"niri-visual-tests",
]
[workspace.package]
version = "0.1.4"
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.80"
[workspace.dependencies]
anyhow = "1.0.81"
bitflags = "2.5.0"
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] }
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.0", 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"
@@ -35,46 +42,53 @@ authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.15.0", features = ["derive"] }
calloop = { version = "0.13.0", 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.7.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.27.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
glam = "0.29.2"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.153"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.4", path = "niri-config" }
niri-ipc = { version = "0.1.4", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.19.2"
pipewire = { version = "0.8.0", optional = true }
png = "0.17.13"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.1"
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 = "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.16"
portable-atomic = { version = "1.10.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.3"
serde.workspace = true
serde_json = "1.0.115"
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "~3.15.2", 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 = "5.2.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -95,20 +109,29 @@ features = [
]
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
xshell = "0.2.5"
approx = "0.5.1"
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-channel", "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.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables the on-demand Tracy profiler instrumentation.
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
# Enables Tracy allocation profiling.
profile-with-tracy-allocations = ["profile-with-tracy"]
# Enables dinit integration (global environment).
dinit = []
@@ -121,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.4"
version = "25.01"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -134,3 +161,14 @@ assets = [
[package.metadata.generate-rpm.requires]
alacritty = "*"
fuzzel = "*"
[package.metadata.deb]
depends = "alacritty, fuzzel"
assets = [
["target/release/niri", "usr/bin/", "755"],
["resources/niri-session", "usr/bin/", "755"],
["resources/niri.desktop", "/usr/share/wayland-sessions/", "644"],
["resources/niri-portals.conf", "/usr/share/xdg-desktop-portal/", "644"],
["resources/niri.service", "/usr/lib/systemd/user/", "644"],
["resources/niri-shutdown.target", "/usr/lib/systemd/user/", "644"],
]
+53 -143
View File
@@ -1,12 +1,16 @@
<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>
![](https://github.com/YaLTeR/niri/assets/1794388/2b246c2c-7cf3-4a11-96eb-ad0c7f2f4ed6)
<p align="center">
<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>
![niri with a few windows open](https://github.com/user-attachments/assets/d142e57d-a25d-4ddb-ab46-311417458211)
## About
@@ -24,26 +28,51 @@ 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 screencasting through xdg-desktop-portal-gnome
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Touchpad gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- Live-reloading config
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
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.
Note that NVIDIA GPUs might have rendering issues.
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
@@ -52,147 +81,28 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
## Packages
## Tile Scrollably Elsewhere
There are several community-maintained distribution packages that you can use to install niri.
Here are some of them:
Here are some other projects which implement a similar workflow:
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
- NixOS Flake: https://github.com/sodiboo/niri-flake
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
## Building
First, install the dependencies for your distribution.
- Ubuntu 23.10:
```sh
sudo apt-get install -y 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
```
- Fedora:
```sh
sudo dnf install 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
```
Next, get latest stable Rust: https://rustup.rs/
Then, build niri with `cargo build --release`.
### NixOS/Nix
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
```
nix run --impure github:guibou/nixGL -- ./results/bin/niri
```
## Installation
The recommended way to install and run niri is as a standalone desktop session.
To do that, put files into the correct directories according to this table.
| File | Destination |
| ---- | ----------- |
| `target/release/niri` | `/usr/bin/` |
| `resources/niri-session` | `/usr/bin/` |
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
| `resources/niri.service` | `/usr/lib/systemd/user/` |
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
Doing this will make niri appear in GDM and, presumably, other display managers.
## Running
`cargo run --release`
Inside an existing desktop session, it will run in a window.
On a TTY, it will run natively.
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
### Session
If you followed the recommended installation steps above, niri should appear in your display manager.
Starting it from there will run niri as a desktop session.
The niri session will autostart apps through the systemd xdg-autostart target.
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
Niri also works with some parts of xdg-desktop-portal-gnome.
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
## Configuration
Please check [this wiki page](https://github.com/YaLTeR/niri/wiki/Configuration:-Overview) for an overview of niri configuration.
It also links to wiki pages containing thorough documentation for all options with examples.
## Default Hotkeys
When running on a TTY, the Mod key is <kbd>Super</kbd>.
When running in a window, the Mod key is <kbd>Alt</kbd>.
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
| Hotkey | Description |
| ------ | ----------- |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column |
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>1</kbd><kbd>9</kbd> | Switch to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused column to a workspace by index |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [papersway]: scrollable tiling on top of sway/i3.
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
[PaperWM]: https://github.com/paperwm/PaperWM
[mako]: https://github.com/emersion/mako
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[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
+5
View File
@@ -0,0 +1,5 @@
ignore-interior-mutability = [
"smithay::desktop::Window",
"smithay::output::Output",
"wayland_server::backend::ClientId",
]
Generated
+21 -95
View File
@@ -1,72 +1,12 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1709274179,
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
@@ -77,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"lastModified": 1733064805,
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
"type": "github"
},
"original": {
@@ -93,42 +33,28 @@
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-analyzer-src": {
"flake": false,
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709219524,
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
"lastModified": 1733106880,
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
+227 -79
View File
@@ -1,106 +1,254 @@
# This flake file is community maintained
# Maintainers:
# Bill Sun (github/billksun)
{
description = "Niri: A scrollable-tiling Wayland compositor.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nix-filter.url = "github:numtide/nix-filter";
fenix = {
url = "github:nix-community/fenix/monthly";
# NOTE: This is not necessary for end users
# You can omit it with `inputs.rust-overlay.follows = ""`
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
crane,
nix-filter,
flake-utils,
fenix,
...
}: let
systems = ["aarch64-linux" "x86_64-linux"];
in
flake-utils.lib.eachSystem systems (
system: let
pkgs = nixpkgs.legacyPackages.${system};
toolchain = fenix.packages.${system}.complete.toolchain;
craneLib = crane.lib.${system}.overrideToolchain toolchain;
outputs =
{
self,
nixpkgs,
nix-filter,
rust-overlay,
}:
let
niri-package =
{
lib,
cairo,
dbus,
libGL,
libdisplay-info,
libinput,
seatd,
libxkbcommon,
mesa,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
withDbus ? true,
withSystemd ? true,
withScreencastSupport ? true,
withDinit ? false,
}:
craneArgs = {
rustPlatform.buildRustPackage {
pname = "niri";
version = self.rev or "dirty";
version = self.shortRev or self.dirtyShortRev or "unknown";
src = nixpkgs.lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(builtins.match "resources" path == null) ||
((craneLib.filterCargoSources path type) &&
(builtins.match "niri-visual-tests" path == null));
src = nix-filter.lib.filter {
root = self;
include = [
"niri-config"
"niri-ipc"
"niri-visual-tests"
"resources"
"src"
./Cargo.lock
./Cargo.toml
];
};
nativeBuildInputs = with pkgs; [
postPatch = ''
patchShebangs resources/niri-session
substituteInPlace resources/niri.service \
--replace-fail '/usr/bin' "$out/bin"
'';
cargoLock = {
# NOTE: This is only used for Git dependencies
allowBuiltinFetchGit = true;
lockFile = ./Cargo.lock;
};
strictDeps = true;
nativeBuildInputs = [
rustPlatform.bindgenHook
pkg-config
autoPatchelfHook
clang
gdk-pixbuf
graphene
gtk4
libadwaita
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
];
buildInputs =
[
cairo
dbus
libGL
libdisplay-info
libinput
seatd
libxkbcommon
mesa # libgbm
pango
wayland
]
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
++ lib.optional withScreencastSupport pipewire
# Also includes libudev
++ lib.optional withSystemd systemd;
runtimeDependencies = with pkgs; [
wayland
mesa
libglvnd # For libEGL
];
buildFeatures =
lib.optional withDbus "dbus"
++ lib.optional withDinit "dinit"
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
++ lib.optional withSystemd "systemd";
buildNoDefaultFeatures = true;
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
# 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
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
''
+ lib.optionalString withSystemd ''
install -Dm755 resources/niri-session $out/bin/niri-session
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
'';
env = {
# Force linking with libEGL and libwayland-client
# so they can be discovered by `dlopen()`
RUSTFLAGS = toString (
map (arg: "-C link-arg=" + arg) [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
]
);
};
passthru = {
providedSessions = [ "niri" ];
};
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/YaLTeR/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
};
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
in {
formatter = pkgs.alejandra;
inherit (nixpkgs) lib;
# Support all Linux systems that the nixpkgs flake exposes
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
checks.niri = niri;
packages.default = niri;
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
{
checks = forAllSystems (system: {
# We use the debug build here to save a bit of time
inherit (self.packages.${system}) niri-debug;
});
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
packages = niri.runtimeDependencies;
devShells = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
inherit (self.packages.${system}) niri;
in
{
default = pkgs.mkShell {
packages = [
# We don't use the toolchain from nixpkgs
# because we prefer a nightly toolchain
# and we *require* a nightly rustfmt
(rust-bin.selectLatestNightlyWith (
toolchain:
toolchain.default.override {
extensions = [
# includes already:
# rustc
# cargo
# rust-std
# rust-docs
# rustfmt-preview
# clippy-preview
"rust-analyzer"
"rust-src"
];
}
))
];
# Force linking to libEGL, which is always dlopen()ed, and to
# libwayland-client, which is always dlopen()ed except by the
# obscure winit backend.
RUSTFLAGS = map (a: "-C link-arg=${a}") [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
];
};
}
);
nativeBuildInputs = [
pkgs.rustPlatform.bindgenHook
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
buildInputs = niri.buildInputs ++ [
pkgs.libadwaita # For `niri-visual-tests`
];
env = {
# 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
#
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
};
};
}
);
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
packages = forAllSystems (
system:
let
niri = nixpkgsFor.${system}.callPackage niri-package { };
in
{
inherit niri;
# NOTE: This is for development purposes only
#
# It is primarily to help with quickly iterating on
# changes made to the above expression - though it is
# also not stripped in order to better debug niri itself
niri-debug = niri.overrideAttrs (
newAttrs: oldAttrs: {
pname = oldAttrs.pname + "-debug";
cargoBuildType = "debug";
cargoCheckType = newAttrs.cargoBuildType;
dontStrip = true;
}
);
default = niri;
}
);
overlays.default = final: _: {
niri = final.callPackage niri-package { };
};
};
}
+8 -3
View File
@@ -9,11 +9,16 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.6.2"
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.4", path = "../niri-ipc" }
regex = "1.10.4"
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]
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>,
}
+2352 -354
View File
File diff suppressed because it is too large Load Diff
+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)
}
}
+111
View File
@@ -0,0 +1,111 @@
use std::fs;
use std::path::PathBuf;
struct KdlCodeBlock {
filename: String,
code: String,
line_number: usize,
must_fail: bool,
}
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
let mut lines = file_contents
.lines()
.map(|line| {
// Removes the > from callouts that might contain ```kdl```
let line = line.trim();
if line.starts_with('>') {
if line.len() == 1 {
""
} else {
&line[2..]
}
} else {
line
}
})
.enumerate();
let mut kdl_code_blocks = vec![];
while let Some((line_number, line)) = lines.next() {
if !line.starts_with("```kdl") {
continue;
}
let mut snippet = String::new();
for (_, line) in lines
.by_ref()
.take_while(|(_, line)| !line.starts_with("```"))
{
snippet.push_str(line);
snippet.push('\n');
}
kdl_code_blocks.push(KdlCodeBlock {
code: snippet,
line_number,
filename: filename.to_string(),
must_fail: line.contains("must-fail"),
});
}
kdl_code_blocks
}
#[test]
fn wiki_docs_parses() {
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
let code_blocks = fs::read_dir(wiki_dir)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
.filter(|file| {
file.path()
.extension()
.map(|ext| ext == "md")
.unwrap_or(false)
})
.flat_map(|file| {
let file_contents = fs::read_to_string(file.path()).unwrap();
let file_path = file.path();
let filename = file_path.to_str().unwrap();
extract_kdl_from_file(&file_contents, filename)
});
let mut errors = vec![];
for KdlCodeBlock {
code,
line_number,
filename,
must_fail,
} in code_blocks
{
if let Err(error) = niri_config::Config::parse(&filename, &code) {
if !must_fail {
errors.push(format!(
"Error parsing wiki KDL code block at {}:{}: {:?}",
filename,
line_number,
miette::Report::new(error)
));
}
} else if must_fail {
errors.push(format!(
"Expected error parsing wiki KDL code block at {}:{}",
filename, line_number
));
}
}
if !errors.is_empty() {
panic!(
"Errors parsing {} wiki KDL code blocks:\n{}",
errors.len(),
errors.join("\n")
);
}
}
+8 -1
View File
@@ -1,15 +1,22 @@
[package]
name = "niri-ipc"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
description = "Types and helpers for interfacing with the niri Wayland compositor."
keywords = ["wayland"]
categories = ["api-bindings", "os"]
readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "0.8.21", optional = true }
serde.workspace = true
serde_json.workspace = true
[features]
clap = ["dep:clap"]
json-schema = ["dep:schemars"]
+16
View File
@@ -0,0 +1,16 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
## Backwards compatibility
This crate follows the niri version.
It is **not** API-stable in terms of the Rust semver.
In particular, expect new struct fields and enum variants to be added in patch version bumps.
Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.1.0"
```
+896 -91
View File
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
//! Helper for blocking communication over the niri socket.
use std::env;
use std::io::{self, BufRead, BufReader, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::{Event, Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Helper for blocking communication over the niri socket.
///
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: UnixStream,
}
impl Socket {
/// Connects to the default niri IPC socket.
///
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
/// [`SOCKET_PATH_ENV`] environment variable.
pub fn connect() -> io::Result<Self> {
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
)
})?;
Self::connect_to(socket_path)
}
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
Ok(Self { stream })
}
/// Sends a request to niri and returns the response.
///
/// Return values:
///
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_string(&request).unwrap();
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
buf.clear();
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
let events = move || {
buf.clear();
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}
+194
View File
@@ -0,0 +1,194 @@
//! Helpers for keeping track of the event stream state.
//!
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
//! you only care about part of the state.
//! 2. Connect to the niri socket and request an event stream.
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
//! 4. Read the fields of the state as needed.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use crate::{Event, KeyboardLayouts, Window, Workspace};
/// Part of the state communicated via the event stream.
pub trait EventStreamStatePart {
/// Returns a sequence of events that replicates this state from default initialization.
fn replicate(&self) -> Vec<Event>;
/// Applies the event to this state.
///
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
/// part of the state.
fn apply(&mut self, event: Event) -> Option<Event>;
}
/// The full state communicated over the event stream.
///
/// Different parts of the state are not guaranteed to be consistent across every single event
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
/// these two events, the workspace active window id refers to a window that does not yet exist in
/// the windows state part.
#[derive(Debug, Default)]
pub struct EventStreamState {
/// State of workspaces.
pub workspaces: WorkspacesState,
/// State of workspaces.
pub windows: WindowsState,
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
}
/// The workspaces state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WorkspacesState {
/// Map from a workspace id to the workspace.
pub workspaces: HashMap<u64, Workspace>,
}
/// The windows state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WindowsState {
/// Map from a window id to the window.
pub windows: HashMap<u64, Window>,
}
/// The keyboard layout state communicated over the event stream.
#[derive(Debug, Default)]
pub struct KeyboardLayoutsState {
/// Configured keyboard layouts.
pub keyboard_layouts: Option<KeyboardLayouts>,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events
}
fn apply(&mut self, event: Event) -> Option<Event> {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
Some(event)
}
}
impl EventStreamStatePart for WorkspacesState {
fn replicate(&self) -> Vec<Event> {
let workspaces = self.workspaces.values().cloned().collect();
vec![Event::WorkspacesChanged { workspaces }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
let output = ws.output.clone();
for ws in self.workspaces.values_mut() {
let got_activated = ws.id == id;
if ws.output == output {
ws.is_active = got_activated;
}
if focused {
ws.is_focused = got_activated;
}
}
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
let ws = self.workspaces.get_mut(&workspace_id);
let ws = ws.expect("changed workspace was missing from the map");
ws.active_window_id = active_window_id;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for WindowsState {
fn replicate(&self) -> Vec<Event> {
let windows = self.windows.values().cloned().collect();
vec![Event::WindowsChanged { windows }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WindowsChanged { windows } => {
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
}
Event::WindowOpenedOrChanged { window } => {
let (id, is_focused) = match self.windows.entry(window.id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
*entry = window;
(entry.id, entry.is_focused)
}
Entry::Vacant(entry) => {
let entry = entry.insert(window);
(entry.id, entry.is_focused)
}
};
if is_focused {
for win in self.windows.values_mut() {
if win.id != id {
win.is_focused = false;
}
}
}
}
Event::WindowClosed { id } => {
let win = self.windows.remove(&id);
win.expect("closed window was missing from the map");
}
Event::WindowFocusChanged { id } => {
for win in self.windows.values_mut() {
win.is_focused = Some(win.id) == id;
}
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for KeyboardLayoutsState {
fn replicate(&self) -> Vec<Event> {
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
} else {
vec![]
}
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
self.keyboard_layouts = Some(keyboard_layouts);
}
Event::KeyboardLayoutSwitched { idx } => {
let kb = self.keyboard_layouts.as_mut();
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
kb.current_idx = idx;
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.4", path = ".." }
niri-config = { version = "0.1.4", 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
+19 -23
View File
@@ -1,14 +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::gradient::GradientRenderElement;
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, Scale, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientAngle {
angle: f32,
@@ -16,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,
@@ -30,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. {
@@ -53,22 +45,26 @@ impl TestCase for GradientAngle {
fn render(
&mut self,
renderer: &mut GlesRenderer,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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);
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
+34 -36
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::gradient::GradientRenderElement;
use niri_config::Color;
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, Scale, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientArea {
progress: f32,
@@ -19,16 +17,15 @@ pub struct GradientArea {
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let mut border = FocusRing::new(niri_config::FocusRing {
pub fn new(_args: Args) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
active_color: Color::new(255, 255, 255, 128),
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
});
border.set_active(true);
Self {
progress: 0.,
@@ -44,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. {
@@ -75,38 +65,46 @@ 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);
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,
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
));
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
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::new(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update(g_size, true);
self.border.update_render_elements(
g_size,
true,
true,
Rectangle::default(),
CornerRadius::default(),
1.,
);
rv.extend(
self.border
.render(
renderer,
Point::from(g_loc),
Scale::from(1.),
size.to_logical(1),
)
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
[BorderRenderElement::new(
area.size,
g_area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_size(rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _),
);
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklab {
gradient_format: GradientInterpolation,
}
impl GradientOklab {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientOklab {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
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::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklabAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklabAlpha {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientOklabAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklchAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklchAlpha {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Longer,
},
}
}
}
impl TestCase for GradientOklchAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklchDecreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchDecreasing {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Decreasing,
},
}
}
}
impl TestCase for GradientOklchDecreasing {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklchIncreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchIncreasing {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Increasing,
},
}
}
}
impl TestCase for GradientOklchIncreasing {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklchLonger {
gradient_format: GradientInterpolation,
}
impl GradientOklchLonger {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Longer,
},
}
}
}
impl TestCase for GradientOklchLonger {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientOklchShorter {
gradient_format: GradientInterpolation,
}
impl GradientOklchShorter {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientOklchShorter {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientSrgb {
gradient_format: GradientInterpolation,
}
impl GradientSrgb {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientSrgb {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
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::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientSrgbAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbAlpha {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientSrgbAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientSrgbLinear {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinear {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientSrgbLinear {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
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::{Physical, Point, Rectangle, Size};
use super::{Args, TestCase};
pub struct GradientSrgbLinearAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinearAlpha {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientSrgbLinearAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> 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::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
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_size(area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+89 -49
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;
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 {
@@ -41,6 +44,12 @@ impl Layout {
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
output.user_data().insert_if_missing(|| OutputName {
connector: String::new(),
make: None,
model: None,
serial: None,
});
let options = Options {
focus_ring: niri_config::FocusRing {
@@ -49,28 +58,31 @@ impl Layout {
},
border: niri_config::Border {
off: false,
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
active_gradient: None,
inactive_gradient: None,
},
..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)));
@@ -85,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| {
@@ -99,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| {
@@ -113,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)));
@@ -129,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)));
@@ -145,25 +157,54 @@ impl Layout {
rv
}
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
self.layout.add_window(window.clone(), width, false);
if window.communicate() {
self.layout.update_window(window.id());
}
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
None,
);
window.communicate();
self.layout.add_window(
window.clone(),
AddWindowTarget::Auto,
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
fn add_window_right_of(
&mut self,
right_of: &TestWindow,
window: TestWindow,
mut window: TestWindow,
width: Option<ColumnWidth>,
) {
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
if window.communicate() {
self.layout.update_window(window.id());
}
let ws = self.layout.active_workspace().unwrap();
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
None,
);
window.communicate();
self.layout.add_window(
window.clone(),
AddWindowTarget::NextTo(right_of.id()),
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
@@ -184,35 +225,34 @@ impl TestCase for Layout {
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win.id());
self.layout.update_window(win.id(), None);
}
}
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
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(
@@ -220,11 +260,11 @@ impl TestCase for Layout {
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout.update_render_elements(Some(&self.output));
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.render_elements(renderer, RenderTarget::Output, true)
.map(|elem| Box::new(elem) as _)
.collect()
}
+18 -1
View File
@@ -1,15 +1,32 @@
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;
pub mod gradient_oklab;
pub mod gradient_oklab_alpha;
pub mod gradient_oklch_alpha;
pub mod gradient_oklch_decreasing;
pub mod gradient_oklch_increasing;
pub mod gradient_oklch_longer;
pub mod gradient_oklch_shorter;
pub mod gradient_srgb;
pub mod gradient_srgb_alpha;
pub mod gradient_srgblinear;
pub mod gradient_srgblinear_alpha;
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 {
+45 -34
View File
@@ -3,12 +3,12 @@ use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri_config::Color;
use niri_config::{Color, FloatOrInt};
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, 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);
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);
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);
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,
@@ -71,20 +64,34 @@ impl Tile {
},
border: niri_config::Border {
off: false,
width: 32,
active_color: Color::new(255, 163, 72, 255),
width: FloatOrInt(32.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
..Default::default()
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), 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) {
self.tile.request_tile_size(Size::from((width, height)));
let size = Size::from((width, height)).to_f64();
self.tile
.update_config(size, 1., self.tile.options().clone());
self.tile.request_tile_size(size, false, None);
self.window.communicate();
}
@@ -92,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, true);
fn advance_animations(&mut self, _current_time: Duration) {
self.tile.advance_animations();
}
fn render(
@@ -101,15 +108,19 @@ impl TestCase for Tile {
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
let size = size.to_f64();
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update(
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
);
self.tile
.render(
renderer,
location,
Scale::from(1.),
size.to_logical(1),
true,
RenderTarget::Output,
)
+16 -13
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 {
let window = TestWindow::freeform(0);
window.request_size(size);
pub fn freeform(args: Args) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.request_size(size);
pub fn fixed_size(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
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);
window.request_size(args.size, false, None);
window.communicate();
Self { window }
}
@@ -37,7 +37,8 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)));
self.window
.request_size(Size::from((width, height)), false, None);
self.window.communicate();
}
@@ -47,7 +48,9 @@ impl TestCase for Window {
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let win_size = self.window.size().to_physical(1);
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
let location = Point::from((size.w - win_size.w, size.h - win_size.h))
.to_f64()
.downscale(2.);
self.window
.render(
+31 -17
View File
@@ -2,23 +2,30 @@
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::tile::Tile;
use cases::window::Window;
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;
use crate::cases::gradient_angle::GradientAngle;
use crate::cases::gradient_area::GradientArea;
use crate::cases::gradient_oklab::GradientOklab;
use crate::cases::gradient_oklab_alpha::GradientOklabAlpha;
use crate::cases::gradient_oklch_alpha::GradientOklchAlpha;
use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing;
use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing;
use crate::cases::gradient_oklch_longer::GradientOklchLonger;
use crate::cases::gradient_oklch_shorter::GradientOklchShorter;
use crate::cases::gradient_srgb::GradientSrgb;
use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha;
use crate::cases::gradient_srgblinear::GradientSrgbLinear;
use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha;
use crate::cases::layout::Layout;
use crate::cases::tile::Tile;
use crate::cases::window::Window;
use crate::cases::TestCase;
mod cases;
@@ -55,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");
@@ -112,12 +118,20 @@ fn build_ui(app: &adw::Application) {
s.add(GradientAngle::new, "Gradient - Angle");
s.add(GradientArea::new, "Gradient - Area");
s.add(GradientSrgb::new, "Gradient - Srgb");
s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear");
s.add(GradientOklab::new, "Gradient - Oklab");
s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter");
s.add(GradientOklchLonger::new, "Gradient - Oklch Longer");
s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing");
s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing");
s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha");
s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha");
s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha");
s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha");
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);
+45 -20
View File
@@ -1,27 +1,29 @@
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::render_helpers::shaders;
use niri::utils::get_monotonic_time;
use niri::animation::Clock;
use niri::render_helpers::{resources, shaders};
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
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| {
@@ -147,7 +158,7 @@ mod imp {
.context("error creating frame")?;
frame
.clear([0.3, 0.3, 0.3, 1.], &[rect])
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
.context("error clearing")?;
for element in elements.iter().rev() {
@@ -157,7 +168,7 @@ mod imp {
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.draw(&mut frame, src, dst, &[damage], &[])
.context("error drawing element")?;
}
}
@@ -186,14 +197,10 @@ mod imp {
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
.context("error creating EGL context")?;
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
.context("error getting supported renderer capabilities")?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")?;
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
Ok(renderer)
@@ -237,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
}
}
+80 -36
View File
@@ -2,15 +2,19 @@ use std::cell::RefCell;
use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement};
use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::RenderTarget;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
@@ -35,7 +39,7 @@ impl TestWindow {
let size = Size::from((100, 200));
let min_size = Size::from((0, 0));
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]);
Self {
id,
@@ -47,7 +51,7 @@ impl TestWindow {
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
})),
}
}
@@ -83,7 +87,7 @@ impl TestWindow {
let mut new_size = inner.size;
if let Some(size) = inner.requested_size.take() {
if let Some(size) = inner.requested_size {
assert!(size.w >= 0);
assert!(size.h >= 0);
@@ -110,14 +114,14 @@ impl TestWindow {
if inner.size != new_size {
inner.size = new_size;
inner.buffer.resize(new_size);
inner.buffer.resize(new_size.to_f64());
rv = true;
}
let mut csd_shadow_size = new_size;
csd_shadow_size.w += inner.csd_shadow_width * 2;
csd_shadow_size.h += inner.csd_shadow_width * 2;
inner.csd_shadow_buffer.resize(csd_shadow_size);
inner.csd_shadow_buffer.resize(csd_shadow_size.to_f64());
rv
}
@@ -145,40 +149,46 @@ impl LayoutElement for TestWindow {
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
) -> SplitElements<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
]
SplitElements {
normal: vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
location
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
.into(),
],
popups: vec![],
}
}
fn request_size(&self, size: Size<i32, Logical>) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
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;
}
@@ -194,7 +204,7 @@ impl LayoutElement for TestWindow {
false
}
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
fn has_ssd(&self) -> bool {
false
@@ -208,9 +218,17 @@ impl LayoutElement for TestWindow {
fn set_activated(&mut self, _active: bool) {}
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 send_pending_configure(&self) {}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
false
@@ -220,10 +238,36 @@ impl LayoutElement for TestWindow {
self.inner.borrow().pending_fullscreen
}
fn requested_size(&self) -> Option<Size<i32, Logical>> {
self.inner.borrow().requested_size
}
fn is_child_of(&self, _parent: &Self) -> bool {
false
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
None
}
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
None
}
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
fn cancel_interactive_resize(&mut self) {}
fn on_commit(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
}
+150
View File
@@ -0,0 +1,150 @@
%bcond_without check
%global cargo_install_lib 0
# We want panic backtraces to work without installing the debuginfo package,
# so we leave the debuginfo in the main binary.
%global debug_package %{nil}
%global __strip /bin/true
# To reduce the file size, do some convincing of rust-srpm-macros
# to leave alone the chosen debug settings from Cargo.toml.
%global rustflags_debuginfo please-remove-me
%global build_rustflags %{shrink:
-Copt-level=%rustflags_opt_level
-Ccodegen-units=%rustflags_codegen_units
-Cstrip=none
%{expr:0%{?_include_frame_pointers} && ("%{_arch}" != "ppc64le" && "%{_arch}" != "s390x" && "%{_arch}" != "i386") ? "-Cforce-frame-pointers=yes" : ""}
-Clink-arg=-Wl,-z,relro
-Clink-arg=-Wl,-z,now
%[0%{?_package_note_status} ? "-Clink-arg=%_package_note_flags" : ""]
--cap-lints=warn
}
# Convince rust-srpm-macros to use Cargo.lock with the Smithay commit.
%global __cargo_common_opts %{?_smp_mflags} -Z avoid-dev-deps --locked
%global version {{{ git_dir_version }}}
Name: niri
Version: %{version}
Release: 1%{?dist}
Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 OR BSL-1.0
# Apache-2.0 OR MIT
# 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 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: (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
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
BuildRequires: cargo-rpm-macros >= 26
BuildRequires: pkgconfig(udev)
BuildRequires: pkgconfig(gbm)
BuildRequires: pkgconfig(xkbcommon)
BuildRequires: wayland-devel
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(dbus-1)
BuildRequires: pkgconfig(systemd)
BuildRequires: pkgconfig(libseat)
BuildRequires: pkgconfig(libdisplay-info)
BuildRequires: pipewire-devel
BuildRequires: pango-devel
BuildRequires: cairo-gobject-devel
# Needed for pipewire-rs
BuildRequires: clang
Requires: mesa-dri-drivers
Requires: mesa-libEGL
# Portal implementations used by niri
Recommends: xdg-desktop-portal-gtk
Recommends: xdg-desktop-portal-gnome
Recommends: gnome-keyring
# Suggested utilities, bound in the default config
Recommends: alacritty
Recommends: fuzzel
Recommends: swaylock
# Suggested utilities
Recommends: swaybg
Recommends: mako
Recommends: swayidle
%description
A scrollable-tiling Wayland compositor.
Windows are arranged in columns on an infinite strip going to the right.
Opening a new window never causes existing windows to resize.
%prep
{{{ git_dir_setup_macro }}}
%cargo_prep -N
# We're doing an online build.
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
%install
%cargo_install
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
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
%files
%license LICENSE
%doc README.md
%doc resources/default-config.kdl
%doc wiki
%{_bindir}/niri
%{_bindir}/niri-session
%{_datadir}/wayland-sessions/niri.desktop
%dir %{_datadir}/xdg-desktop-portal
%{_datadir}/xdg-desktop-portal/niri-portals.conf
%{_userunitdir}/niri.service
%{_userunitdir}/niri-shutdown.target
%changelog
{{{ git_dir_changelog }}}
+65 -11
View File
@@ -21,25 +21,41 @@ input {
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
touchpad {
// off
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
@@ -60,8 +76,8 @@ input {
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@120.030"
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// You can use integer or fractional scale, for example use 1.5 for 150% scale.
scale 2
// Transform allows to rotate the output counter-clockwise, valid values are:
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
@@ -107,6 +123,9 @@ layout {
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
@@ -147,6 +166,7 @@ layout {
// The angle is the same as in linear-gradient, and is optional,
// defaulting to 180 (top-to-bottom gradient).
// You can use any CSS linear-gradient tool on the web to set these up.
// Changing the color space is also supported, check the wiki for more info.
//
// active-gradient from="#80c8ff" to="#bbddff" angle=45
@@ -187,11 +207,14 @@ layout {
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// spawn-at-startup "alacritty" "-e" "fish"
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
// This option will also fix border/focus ring drawing behind some semitransparent windows.
// After enabling or disabling this, you need to restart the apps for this to take effect.
// prefer-no-csd
// You can change the path where screenshots are saved.
@@ -227,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 {
@@ -239,6 +271,13 @@ window-rule {
// block-out-from "screencast"
}
// Example: enable rounded corners for all windows.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
binds {
// Keys consist of modifiers separated by + signs, followed by an XKB key name
// in the end. To find an XKB name for a particular key, you may use a program
@@ -259,12 +298,16 @@ binds {
Mod+D { spawn "fuzzel"; }
Super+Alt+L { spawn "swaylock"; }
// You can also use a shell:
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
// Example volume keys mappings for PipeWire & WirePlumber.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
// The allow-when-locked=true property makes them work even when the session is locked.
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
Mod+Q { close-window; }
@@ -407,14 +450,20 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// 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; }
// There are also commands that consume or expel a single window to the side.
// Mod+BracketLeft { consume-or-expel-window-left; }
// Mod+BracketRight { consume-or-expel-window-right; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
@@ -434,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.
@@ -448,6 +501,7 @@ binds {
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
+8
View File
@@ -0,0 +1,8 @@
type = process
command = niri --session
restart = false
working-dir = $HOME
depends-on = dbus
after = niri-shutdown
chain-to = niri-shutdown
options: always-chain
+3
View File
@@ -0,0 +1,3 @@
type = scripted
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
restart = false
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="mutter_x11_interop">
<description summary="X11 interoperability helper">
This protocol is intended to be used by the portal backend to map Wayland
dialogs as modal dialogs on top of X11 windows.
</description>
<interface name="mutter_x11_interop" version="1">
<description summary="X11 interoperability helper"/>
<request name="destroy" type="destructor"/>
<request name="set_x11_parent">
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="xwindow" type="uint"/>
</request>
</interface>
</protocol>
+2
View File
@@ -1,3 +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;
+47 -27
View File
@@ -11,31 +11,51 @@ if [ -n "$SHELL" ] &&
fi
fi
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
exit 1
# Try to detect the service manager that is being used
if hash systemctl &> /dev/null; then
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
exit 1
fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Import the login manager environment.
systemctl --user import-environment
# DBus activation environment is independent from systemd. While most of
# dbus-activated services are already using `SystemdService` directive, some
# still don't and thus we should set the dbus environment with a separate
# command.
if hash dbus-update-activation-environment 2>/dev/null; then
dbus-update-activation-environment --all
fi
# Start niri and wait for it to terminate.
systemctl --user --wait start niri.service
# Force stop of graphical-session.target.
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
elif hash dinitctl &> /dev/null; then
# Check that the user dinit daemon is running
if ! pgrep -u $(id -u) dinit &> /dev/null; then
echo "dinit user daemon is not running."
exit 1
fi
# Make sure there's no already running session.
if dinitctl --user is-started niri &> /dev/null; then
echo 'A niri session is already running.'
exit 1
fi
# Start niri
dinitctl --user start niri
else
echo "No systemd or dinit detected, please use niri --session instead."
fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Import the login manager environment.
systemctl --user import-environment
# DBus activation environment is independent from systemd. While most of
# dbus-activated services are already using `SystemdService` directive, some
# still don't and thus we should set the dbus environment with a separate
# command.
if hash dbus-update-activation-environment 2>/dev/null; then
dbus-update-activation-environment --all
fi
# Start niri and wait for it to terminate.
systemctl --user --wait start niri.service
# Force stop of grahical-session.target.
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
+6
View File
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
</head>
</html>
+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));
}
}
+168 -116
View File
@@ -1,23 +1,27 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
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)]
#[derive(Debug, Clone)]
pub struct Animation {
from: f64,
to: f64,
initial_velocity: f64,
is_off: bool,
duration: Duration,
/// Time until the animation first reaches `to`.
///
/// Best effort; not always exactly precise.
clamped_duration: Duration,
start_time: Duration,
current_time: Duration,
clock: Clock,
kind: Kind,
}
@@ -35,110 +39,165 @@ enum Kind {
#[derive(Debug, Clone, Copy)]
pub enum Curve {
Linear,
EaseOutQuad,
EaseOutCubic,
EaseOutExpo,
}
impl Animation {
pub fn new(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
default: 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(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
return Self::ease(from, to, 0, Curve::EaseOutCubic);
rv.is_off = true;
return rv;
}
// Resolve defaults.
let (kind, easing_defaults) = match (config.kind, default.kind) {
// Configured spring.
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
// Configured nothing, defaults spring.
(
niri_config::AnimationKind::Easing(easing),
defaults @ niri_config::AnimationKind::Spring(_),
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
// Configured easing or nothing, defaults easing.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Easing(defaults),
) => (configured, Some(defaults)),
// Configured easing, defaults spring.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Spring(_),
) => (configured, None),
};
rv.replace_config(config);
rv
}
match kind {
pub fn replace_config(&mut self, config: niri_config::Animation) {
self.is_off = config.off;
if config.off {
self.duration = Duration::ZERO;
self.clamped_duration = Duration::ZERO;
return;
}
let start_time = self.start_time;
match config.kind {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
let spring = Spring {
from,
to,
initial_velocity,
from: self.from,
to: self.to,
initial_velocity: self.initial_velocity,
params,
};
Self::spring(spring)
*self = Self::spring(self.clock.clone(), spring);
}
niri_config::AnimationKind::Easing(p) => {
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
Self::ease(from, to, u64::from(duration_ms), curve)
*self = Self::ease(
self.clock.clone(),
self.from,
self.to,
self.initial_velocity,
u64::from(p.duration_ms),
Curve::from(p.curve),
);
}
}
self.start_time = start_time;
}
/// Restarts the animation using the previous config.
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
if self.is_off {
return self.clone();
}
// 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,
self.duration.as_millis() as u64,
curve,
),
Kind::Spring(spring) => {
let spring = Spring {
from: self.from,
to: self.to,
initial_velocity: self.initial_velocity,
params: spring.params,
};
Self::spring(self.clock.clone(), spring)
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let threshold = 0.001; // FIXME
Self::decelerate(
self.clock.clone(),
from,
initial_velocity,
deceleration_rate,
threshold,
)
}
}
}
pub fn ease(from: f64, to: 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 };
Self {
from,
to,
initial_velocity,
is_off: false,
duration,
start_time: now,
current_time: now,
// Our current curves never overshoot.
clamped_duration: duration,
start_time: clock.now(),
clock,
kind,
}
}
pub fn spring(spring: Spring) -> 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 spring(clock: Clock, spring: Spring) -> Self {
let _span = tracy_client::span!("Animation::spring");
let duration = spring.duration();
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
let kind = Kind::Spring(spring);
Self {
from: spring.from,
to: spring.to,
initial_velocity: spring.initial_velocity,
is_off: false,
duration,
start_time: now,
current_time: now,
clamped_duration,
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 {
@@ -157,74 +216,30 @@ impl Animation {
Self {
from,
to,
initial_velocity,
is_off: false,
duration,
start_time: now,
current_time: now,
clamped_duration: duration,
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;
pub fn is_done(&self) -> bool {
if self.clock.should_complete_instantly() {
return true;
}
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;
self.clock.now() >= self.start_time + self.duration
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
pub fn is_clamped_done(&self) -> bool {
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.clamped_duration
}
pub fn value(&self) -> f64 {
@@ -232,7 +247,7 @@ impl Animation {
return self.to;
}
let passed = self.current_time - self.start_time;
let passed = self.clock.now().saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
@@ -241,7 +256,19 @@ impl Animation {
let x = (passed / total).clamp(0., 1.);
curve.y(x) * (self.to - self.from) + self.from
}
Kind::Spring(spring) => spring.value_at(passed),
Kind::Spring(spring) => {
let value = spring.value_at(passed);
// Protect against numerical instability.
let range = (self.to - self.from) * 10.;
let a = self.from - range;
let b = self.to + range;
if self.from <= self.to {
value.clamp(a, b)
} else {
value.clamp(b, a)
}
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
@@ -253,6 +280,17 @@ impl Animation {
}
}
/// Returns a value that stops at the target value after first reaching it.
///
/// Best effort; not always exactly precise.
pub fn clamped_value(&self) -> f64 {
if self.is_clamped_done() {
return self.to;
}
self.value()
}
pub fn to(&self) -> f64 {
self.to
}
@@ -261,11 +299,23 @@ impl Animation {
pub fn from(&self) -> f64 {
self.from
}
pub fn offset(&mut self, offset: f64) {
self.from += offset;
self.to += offset;
if let Kind::Spring(spring) = &mut self.kind {
spring.from += offset;
spring.to += offset;
}
}
}
impl Curve {
pub fn y(self, x: f64) -> f64 {
match self {
Curve::Linear => x,
Curve::EaseOutQuad => EaseOutQuad.y(x),
Curve::EaseOutCubic => EaseOutCubic.y(x),
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
}
@@ -275,6 +325,8 @@ impl Curve {
impl From<niri_config::AnimationCurve> for Curve {
fn from(value: niri_config::AnimationCurve) -> Self {
match value {
niri_config::AnimationCurve::Linear => Curve::Linear,
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
}
+31
View File
@@ -96,6 +96,37 @@ impl Spring {
Duration::from_secs_f64(x1)
}
/// Computes and returns the duration until the spring reaches its target position.
pub fn clamped_duration(&self) -> Option<Duration> {
let beta = self.params.damping / (2. * self.params.mass);
if beta.abs() <= f64::EPSILON || beta < 0. {
return Some(Duration::MAX);
}
if (self.to - self.from).abs() <= f64::EPSILON {
return Some(Duration::ZERO);
}
// The first frame is not that important and we avoid finding the trivial 0 for in-place
// animations.
let mut i = 1u16;
let mut y = self.oscillate(f64::from(i) / 1000.);
while (self.to - self.from > f64::EPSILON && self.to - y > self.params.epsilon)
|| (self.from - self.to > f64::EPSILON && y - self.to > self.params.epsilon)
{
if i > 3000 {
return None;
}
i += 1;
y = self.oscillate(f64::from(i) / 1000.);
}
Some(Duration::from_millis(u64::from(i)))
}
/// Returns the spring position at a given time in seconds.
fn oscillate(&self, t: f64) -> f64 {
let b = self.params.damping;
+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()
}
}
+59 -1
View File
@@ -9,6 +9,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
pub mod tty;
pub use tty::Tty;
@@ -16,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)]
@@ -31,13 +36,29 @@ pub enum RenderResult {
Skipped,
}
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutputId(u64);
impl OutputId {
fn next() -> OutputId {
OutputId(OUTPUT_ID_COUNTER.next())
}
pub fn get(self) -> u64 {
self.0
}
}
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.init(niri),
Backend::Winit(winit) => winit.init(niri),
Backend::Headless(headless) => headless.init(niri),
}
}
@@ -45,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(),
}
}
@@ -55,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),
}
}
@@ -67,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),
}
}
@@ -74,6 +98,7 @@ impl Backend {
match self {
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
Backend::Headless(_) => CompositorMod::Super,
}
}
@@ -81,6 +106,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.change_vt(vt),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -88,6 +114,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.suspend(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -95,6 +122,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.toggle_debug_tint(),
Backend::Winit(winit) => winit.toggle_debug_tint(),
Backend::Headless(_) => (),
}
}
@@ -102,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),
}
}
@@ -109,6 +138,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -116,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(),
}
}
@@ -127,6 +158,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
Backend::Headless(_) => None,
}
}
@@ -134,6 +166,15 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -141,6 +182,15 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
if let Self::Tty(v) = self {
Some(v)
} else {
None
}
}
@@ -159,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")
}
}
}
+707 -306
View File
File diff suppressed because it is too large Load Diff
+48 -16
View File
@@ -3,9 +3,8 @@ use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -15,11 +14,13 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::reexports::winit::window::Window;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, RenderResult};
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::{shaders, RenderTarget};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -35,11 +36,11 @@ impl Winit {
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
let builder = Window::default_attributes()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
let (backend, winit) = winit::init_from_builder(builder)?;
let (backend, winit) = winit::init_from_attributes(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -58,13 +59,21 @@ impl Winit {
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
output.user_data().insert_if_missing(|| OutputName {
connector: "winit".to_string(),
make: Some("Smithay".to_string()),
model: Some("Winit".to_string()),
serial: None,
});
let physical_properties = output.physical_properties();
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
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: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
@@ -73,6 +82,8 @@ impl Winit {
is_preferred: true,
}],
current_mode: Some(0),
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
},
)])));
@@ -95,7 +106,7 @@ impl Winit {
{
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
let output = ipc_outputs.get_mut("winit").unwrap();
let output = ipc_outputs.values_mut().next().unwrap();
let mode = &mut output.modes[0];
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
@@ -130,9 +141,24 @@ impl Winit {
warn!("error binding renderer wl_display: {err}");
}
resources::init(renderer);
shaders::init(renderer);
niri.add_output(self.output.clone(), None);
let config = self.config.borrow();
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
shaders::set_custom_resize_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
shaders::set_custom_close_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
shaders::set_custom_open_program(renderer, Some(src));
}
drop(config);
niri.layout.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
pub fn seat_name(&self) -> String {
@@ -150,13 +176,19 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
let output_state = niri.output_state.get_mut(output).unwrap();
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
@@ -176,17 +208,17 @@ impl Winit {
.wait_for_frame_completion_before_queueing
{
let _span = tracy_client::span!("wait for completion");
res.sync.wait();
if let Err(err) = res.sync.wait() {
warn!("error waiting for frame completion: {err:?}");
}
}
self.backend.submit(Some(&damage)).unwrap();
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(),
);
+44 -7
View File
@@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -13,6 +13,9 @@ use crate::utils::version;
#[command(subcommand_help_heading = "Subcommands")]
pub struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
///
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
/// command line argument takes precedence.
#[arg(short, long)]
pub config: Option<PathBuf>,
/// Import environment globally to systemd and D-Bus, run D-Bus services.
@@ -32,12 +35,6 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
@@ -46,6 +43,15 @@ pub enum Sub {
#[arg(short, long)]
json: bool,
},
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
///
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
/// command line argument takes precedence.
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
@@ -54,6 +60,16 @@ pub enum Sub {
pub enum Msg {
/// List connected outputs.
Outputs,
/// List workspaces.
Workspaces,
/// List open windows.
Windows,
/// List open layer-shell surfaces.
Layers,
/// Get the configured keyboard layouts.
KeyboardLayouts,
/// Print information about the focused output.
FocusedOutput,
/// Print information about the focused window.
FocusedWindow,
/// Perform an action.
@@ -61,4 +77,25 @@ pub enum Msg {
#[command(subcommand)]
action: Action,
},
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
/// configuration subsequently changes in the config file, these temporary changes will be
/// forgotten.
Output {
/// Output name.
///
/// Run `niri msg outputs` to see the output names.
#[arg()]
output: String,
/// Configuration to apply.
#[command(subcommand)]
action: OutputAction,
},
/// Start continuously receiving events from the compositor.
EventStream,
/// Print the version of the running niri instance.
Version,
/// Request an error from the running niri instance.
RequestError,
}
+1 -1
View File
@@ -142,7 +142,7 @@ impl CursorManager {
.unwrap()
}
/// Currenly used cursor_image as a cursor provider.
/// Currently used cursor_image as a cursor provider.
pub fn cursor_image(&self) -> &CursorImageStatus {
&self.current_cursor
}
+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());
+82
View File
@@ -0,0 +1,82 @@
use std::collections::HashMap;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{SerializeDict, Type, Value};
use super::Start;
pub struct Introspect {
to_niri: calloop::channel::Sender<IntrospectToNiri>,
from_niri: async_channel::Receiver<NiriToIntrospect>,
}
pub enum IntrospectToNiri {
GetWindows,
}
pub enum NiriToIntrospect {
Windows(HashMap<u64, WindowProperties>),
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
pub struct WindowProperties {
/// Window title.
pub title: String,
/// Window app ID.
///
/// This is actually the name of the .desktop file, and Shell does internal tracking to match
/// Wayland app IDs to desktop files. We don't do that yet, which is the reason why
/// xdg-desktop-portal-gnome's window list is missing icons.
#[zvariant(rename = "app-id")]
pub app_id: String,
}
#[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) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
match self.from_niri.recv().await {
Ok(NiriToIntrospect::Windows(windows)) => Ok(windows),
Err(err) => {
warn!("error receiving message from niri: {err:?}");
Err(fdo::Error::Failed("internal error".to_owned()))
}
}
}
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
// needed for the event stream IPC anyway).
#[zbus(signal)]
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
impl Introspect {
pub fn new(
to_niri: calloop::channel::Sender<IntrospectToNiri>,
from_niri: async_channel::Receiver<NiriToIntrospect>,
) -> Self {
Self { to_niri, from_niri }
}
}
impl Start for Introspect {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/gnome/Shell/Introspect", self)?;
conn.request_name_with_flags("org.gnome.Shell.Introspect", flags)?;
Ok(conn)
}
}
+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,
+19 -8
View File
@@ -1,9 +1,10 @@
use zbus::blocking::Connection;
use zbus::Interface;
use zbus::object_server::Interface;
use crate::niri::State;
pub mod freedesktop_screensaver;
pub mod gnome_shell_introspect;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
@@ -14,6 +15,7 @@ pub mod mutter_screen_cast;
use mutter_screen_cast::ScreenCast;
use self::freedesktop_screensaver::ScreenSaver;
use self::gnome_shell_introspect::Introspect;
use self::mutter_display_config::DisplayConfig;
use self::mutter_service_channel::ServiceChannel;
@@ -27,6 +29,7 @@ pub struct DBusServers {
pub conn_display_config: Option<Connection>,
pub conn_screen_saver: Option<Connection>,
pub conn_screen_shot: Option<Connection>,
pub conn_introspect: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
}
@@ -66,24 +69,32 @@ impl DBusServers {
let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri);
dbus.conn_screen_shot = try_start(screenshot);
let (to_niri, from_introspect) = calloop::channel::channel();
let (to_introspect, from_niri) = async_channel::unbounded();
niri.event_loop
.insert_source(from_introspect, move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => {
state.on_introspect_msg(&to_introspect, msg)
}
calloop::channel::Event::Closed => (),
})
.unwrap();
let introspect = Introspect::new(to_niri, from_niri);
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, {
let to_niri = to_niri.clone();
move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => {
state.on_screen_cast_msg(&to_niri, msg)
}
calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg),
calloop::channel::Event::Closed => (),
}
})
.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");
}
}
+72 -28
View File
@@ -3,11 +3,13 @@ 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;
use crate::utils::is_laptop_panel;
pub struct DisplayConfig {
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
@@ -42,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,
@@ -57,24 +59,20 @@ impl DisplayConfig {
.ipc_outputs
.lock()
.unwrap()
.iter()
.values()
// Take only enabled outputs.
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
.map(|(c, output)| {
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
.map(|output| {
// Loosely matches the check in Mutter.
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
// FIXME: use proper serial when we have libdisplay-info.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut properties = HashMap::new();
if is_laptop_panel {
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
);
}
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
@@ -110,8 +108,16 @@ impl DisplayConfig {
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let monitor = Monitor {
names: (c.clone(), String::new(), String::new(), serial),
names: (connector, make, model, serial),
modes,
properties,
};
@@ -143,23 +149,16 @@ impl DisplayConfig {
})
.collect();
// Sort the built-in monitor first, then by connector name.
monitors.sort_unstable_by(|a, b| {
let a_is_builtin = a.0.properties.contains_key("display-name");
let b_is_builtin = b.0.properties.contains_key("display-name");
a_is_builtin
.cmp(&b_is_builtin)
.reverse()
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
});
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
#[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 {
@@ -182,3 +181,48 @@ impl Start for DisplayConfig {
Ok(conn)
}
}
// Adapted from Mutter.
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
if is_laptop_panel {
return String::from("Built-in display");
}
let make = &output.make;
let model = &output.model;
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
format_diagonal(diagonal)
}) {
format!("{make} {diagonal}")
} else if model != "Unknown" {
format!("{make} {model}")
} else {
make.clone()
}
}
fn format_diagonal(diagonal_inches: f64) -> String {
let known = [12.1, 13.3, 15.6];
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
format!("{d:.1}")
} else {
format!("{}", diagonal_inches.round() as u32)
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
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″");
}
}
+117 -32
View File
@@ -4,10 +4,10 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
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;
@@ -47,15 +47,40 @@ struct RecordMonitorProperties {
_is_recording: Option<bool>,
}
#[derive(Debug, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
struct RecordWindowProperties {
#[zvariant(rename = "window-id")]
window_id: u64,
#[zvariant(rename = "cursor-mode")]
cursor_mode: Option<CursorMode>,
#[zvariant(rename = "is-recording")]
_is_recording: Option<bool>,
}
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct Stream {
// FIXME: update on scale changes and whatnot.
output: niri_ipc::Output,
target: StreamTarget,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Clone)]
enum StreamTarget {
// FIXME: update on scale changes and whatnot.
Output(niri_ipc::Output),
Window { id: u64 },
}
#[derive(Debug, Clone)]
pub enum StreamTargetId {
Output { name: String },
Window { id: u64 },
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
@@ -68,17 +93,16 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
output: String,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
signal_ctx: SignalEmitter<'static>,
},
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
#[interface(name = "org.gnome.Mutter.ScreenCast")]
impl ScreenCast {
async fn create_session(
&self,
@@ -113,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");
@@ -152,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();
}
@@ -168,7 +192,11 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
let output = {
let ipc_outputs = self.ipc_outputs.lock().unwrap();
ipc_outputs.values().find(|o| o.name == connector).cloned()
};
let Some(output) = output else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
@@ -176,16 +204,16 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
NUMBER.fetch_add(1, Ordering::SeqCst)
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
let target = StreamTarget::Output(output);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -202,22 +230,68 @@ impl Session {
Ok(path)
}
#[dbus_interface(signal)]
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
async fn record_window(
&mut self,
#[zbus(object_server)] server: &ObjectServer,
properties: RecordWindowProperties,
) -> fdo::Result<OwnedObjectPath> {
debug!(?properties, "record_window");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let target = StreamTarget::Window {
id: properties.window_id,
};
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
self.streams.lock().unwrap().push((stream, iface));
}
Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())),
Err(err) => {
return Err(fdo::Error::Failed(format!(
"error creating stream object: {err:?}"
)))
}
}
Ok(path)
}
#[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 {
let logical = self.output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
match &self.target {
StreamTarget::Output(output) => {
let logical = output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
}
}
StreamTarget::Window { .. } => {
// Does any consumer need this?
StreamParameters {
position: (0, 0),
size: (1, 1),
}
}
}
}
}
@@ -275,27 +349,27 @@ impl Drop for Session {
}
impl Stream {
pub fn new(
output: niri_ipc::Output,
fn new(
target: StreamTarget,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
output,
target,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
to_niri,
}
}
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;
}
let msg = ScreenCastToNiri::StartCast {
session_id,
output: self.output.name.clone(),
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
@@ -305,3 +379,14 @@ impl Stream {
}
}
}
impl StreamTarget {
fn make_id(&self) -> StreamTargetId {
match self {
StreamTarget::Output(output) => StreamTargetId::Output {
name: output.name.clone(),
},
StreamTarget::Window { id } => StreamTargetId::Window { id: *id },
}
}
}
+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()?;
+24 -2
View File
@@ -7,10 +7,11 @@ use crate::utils::get_monotonic_time;
pub struct FrameClock {
last_presentation_time: Option<Duration>,
refresh_interval_ns: Option<NonZeroU64>,
vrr: bool,
}
impl FrameClock {
pub fn new(refresh_interval: Option<Duration>) -> Self {
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
@@ -21,6 +22,7 @@ impl FrameClock {
Self {
last_presentation_time: None,
refresh_interval_ns,
vrr,
}
}
@@ -29,6 +31,19 @@ impl FrameClock {
.map(|r| Duration::from_nanos(r.get()))
}
pub fn set_vrr(&mut self, vrr: bool) {
if self.vrr == vrr {
return;
}
self.vrr = vrr;
self.last_presentation_time = None;
}
pub fn vrr(&self) -> bool {
self.vrr
}
pub fn presented(&mut self, presentation_time: Duration) {
if presentation_time.is_zero() {
// Not interested in these.
@@ -71,6 +86,13 @@ impl FrameClock {
let since_last_ns =
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
last_presentation_time + Duration::from_nanos(to_next_ns)
// If VRR is enabled and more than one frame passed since last presentation, assume that we
// can present immediately.
if self.vrr && to_next_ns > refresh_interval_ns {
now
} else {
last_presentation_time + Duration::from_nanos(to_next_ns)
}
}
}
+300 -67
View File
@@ -1,22 +1,29 @@
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;
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Client, Resource};
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, send_surface_state,
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, remove_pre_commit_hook,
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
SurfaceAttributes,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
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;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
@@ -35,50 +42,21 @@ impl CompositorHandler for State {
}
if let Some(output) = self.niri.output_for_root(&root) {
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
send_scale_transform(surface, data, scale, transform);
});
}
}
fn new_surface(&mut self, surface: &WlSurface) {
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
.cached_state
.pending::<SurfaceAttributes>()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok(),
_ => None,
})
});
if let Some(dmabuf) = maybe_dmabuf {
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
let client = surface.client().unwrap();
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
}
}
}
});
self.add_default_dmabuf_pre_commit_hook(surface);
}
fn commit(&mut self, surface: &WlSurface) {
let _span = tracy_client::span!("CompositorHandler::commit");
trace!(surface = ?surface.id(), "commit");
on_commit_buffer_handler::<Self>(surface);
self.backend.early_import(surface);
@@ -92,6 +70,11 @@ impl CompositorHandler for State {
root_surface = parent;
}
// Update the cached root surface.
self.niri
.root_surface
.insert(surface.clone(), root_surface.clone());
if surface == &root_surface {
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
@@ -104,31 +87,76 @@ 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 (rules, width, is_full_width, output) =
let toplevel = window.toplevel().expect("no X11 support");
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,
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(rules, width, is_full_width, output)
// Check that the workspace still exists.
let workspace_id = workspace_name
.as_deref()
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
.map(|(_, ws)| ws.id());
(rules, width, height, is_full_width, output, workspace_id)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None)
(ResolvedWindowRules::empty(), None, None, false, None, None)
};
let parent = window
.toplevel()
.expect("no x11 support")
// 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))
// Only consider the parent if we configured the window for the same
@@ -142,30 +170,40 @@ impl CompositorHandler for State {
})
.map(|(mapped, _)| mapped.window.clone());
let mapped = Mapped::new(window, rules);
// The mapped pre-commit hook deals with dma-bufs on its own.
self.remove_default_dmabuf_pre_commit_hook(toplevel.wl_surface());
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
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)
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);
@@ -187,7 +225,8 @@ impl CompositorHandler for State {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel.
let is_mapped =
@@ -197,9 +236,44 @@ impl CompositorHandler for State {
false
});
// Must start the close animation before window.on_commit().
let transaction = Transaction::new();
if !is_mapped {
let blocker = transaction.blocker();
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window, blocker);
});
}
window.on_commit();
if !is_mapped {
// The toplevel got unmapped.
self.niri.layout.remove_window(&window);
//
// Test client: wleird-unmap.
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: id.get(),
});
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface);
// If this is the only instance, then this transaction will complete
// immediately, so no need to set the timer.
if !transaction.is_last() {
transaction.register_deadline_timer(&self.niri.event_loop);
}
if was_active {
self.maybe_warp_cursor_to_focus();
}
// Newly-unmapped toplevels must perform the initial commit-configure sequence
// afresh.
@@ -210,11 +284,44 @@ impl CompositorHandler for State {
return;
}
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, buffer_delta)
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
}
// The toplevel remains mapped.
self.niri.layout.update_window(&window);
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;
@@ -229,7 +336,7 @@ impl CompositorHandler for State {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.layout.update_window(&window, None);
self.niri.queue_redraw(&output);
return;
}
@@ -240,36 +347,108 @@ impl CompositorHandler for State {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(&output.clone());
}
return;
}
// This might be a layer-shell surface.
self.layer_shell_handle_commit(surface);
if self.layer_shell_handle_commit(surface) {
return;
}
// This might be a cursor surface.
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
{
if matches!(
&self.niri.cursor_manager.cursor_image(),
CursorImageStatus::Surface(s) if s == &root_surface
) {
// In case the cursor surface has been committed handle the role specific
// buffer offset by applying the offset on the cursor image hotspot
if surface == &root_surface {
with_states(surface, |states| {
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
if let Some(mut cursor_image_attributes) =
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
{
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
if let Some(buffer_delta) = buffer_delta {
cursor_image_attributes.hotspot -= buffer_delta;
}
}
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a DnD icon surface.
if self.niri.dnd_icon.as_ref() == Some(surface) {
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
// In case the dnd surface has been committed handle the role specific
// buffer offset by applying the offset on the dnd icon offset
if surface == &dnd_icon.surface {
with_states(&dnd_icon.surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take()
.unwrap_or_default();
dnd_icon.offset += buffer_delta;
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a lock surface.
if self.niri.is_locked() {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == surface {
if lock_surface.wl_surface() == &root_surface {
self.niri.queue_redraw(&output.clone());
break;
return;
}
}
}
}
}
fn destroyed(&mut self, surface: &WlSurface) {
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
// when that happens, so that the closing animation includes all these subsurfaces.
//
// Test client: alacritty with CSD <= 0.13 (it was fixed in winit afterwards:
// https://github.com/rust-windowing/winit/pull/3625).
//
// This is still not perfect, as this function is called already after the (first)
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
}
}
self.niri
.root_surface
.retain(|k, v| k != surface && v != surface);
self.niri.dmabuf_pre_commit_hook.remove(surface);
}
}
impl BufferHandler for State {
@@ -284,3 +463,57 @@ impl ShmHandler for State {
delegate_compositor!(State);
delegate_shm!(State);
impl State {
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
.cached_state
.get::<SurfaceAttributes>()
.pending()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
_ => None,
})
});
if let Some(dmabuf) = maybe_dmabuf {
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
if let Some(client) = surface.client() {
let res =
state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
trace!("added default dmabuf blocker");
}
}
}
}
});
let s = surface.clone();
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
error!("tried to add dmabuf pre-commit hook when there was already one");
remove_pre_commit_hook(surface, prev);
}
}
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, hook);
} else {
error!("tried to remove dmabuf pre-commit hook but there was none");
}
}
}
+120 -38
View File
@@ -1,16 +1,19 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, WindowSurfaceType};
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::compositor::{get_parent, with_states};
use smithay::wayland::shell::wlr_layer::{
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -24,17 +27,30 @@ impl WlrLayerShellHandler for State {
_layer: Layer,
namespace: String,
) {
let output = wl_output
.as_ref()
.and_then(Output::from_resource)
.or_else(|| self.niri.layout.active_output().cloned())
.unwrap();
let output = if let Some(wl_output) = &wl_output {
Output::from_resource(wl_output)
} else {
self.niri.layout.active_output().cloned()
};
let Some(output) = output else {
warn!("no output for new layer surface, closing");
surface.send_close();
return;
};
let wl_surface = surface.wl_surface().clone();
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
assert!(is_new);
let mut map = layer_map_for_output(&output);
map.map_layer(&LayerSurface::new(surface, namespace))
.unwrap();
}
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
let wl_surface = surface.wl_surface();
self.niri.unmapped_layer_surfaces.remove(wl_surface);
let output = if let Some((output, mut map, layer)) =
self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
@@ -45,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
@@ -55,58 +72,123 @@ impl WlrLayerShellHandler for State {
}
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
self.unconstrain_popup(&popup);
self.unconstrain_popup(&PopupKind::Xdg(popup));
}
}
delegate_layer_shell!(State);
impl State {
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
let Some(output) = self
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
let mut root_surface = surface.clone();
while let Some(parent) = get_parent(&root_surface) {
root_surface = parent;
}
let output = self
.niri
.layout
.outputs()
.find(|o| {
let map = layer_map_for_output(o);
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
.is_some()
})
.cloned()
else {
return;
.cloned();
let Some(output) = output else {
return false;
};
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if surface == &root_surface {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
let mut map = layer_map_for_output(&output);
let mut map = layer_map_for_output(&output);
// Arrange the layers before sending the initial configure to respect any size the
// client may have sent.
map.arrange();
// arrange the layers before sending the initial configure
// to respect any size the client may have sent
map.arrange();
// send the initial configure if relevant
if !initial_configure_sent {
let layer = map
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
.unwrap();
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
});
if initial_configure_sent {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
layer.layer_surface().send_configure();
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
// a big deal since panels generally only open once at the start of the
// session.
//
// Note that:
// 1) Exclusive layer surfaces already get focus automatically in
// update_keyboard_focus().
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand
// surfaces in update_keyboard_focus(), so we don't need to check for that
// here.
//
// https://github.com/YaLTeR/niri/issues/641
let on_demand = layer.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::OnDemand;
if was_unmapped && on_demand {
// I guess it'd make sense to check that no higher-layer on-demand surface
// has focus, but Smithay's Layer doesn't implement Ord so this would be a
// little annoying.
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 {
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_scale_transform(surface, data, scale, transform);
});
layer.layer_surface().send_configure();
}
drop(map);
// This will call queue_redraw() inside.
self.niri.output_resized(&output);
} else {
// This is an unsync layer-shell subsurface.
self.niri.queue_redraw(&output);
}
drop(map);
self.niri.output_resized(&output);
true
}
}
+276 -43
View File
@@ -7,29 +7,36 @@ use std::io::Write;
use std::os::fd::OwnedFd;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::backend::input::TabletToolDescriptor;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::wayland::compositor::{get_parent, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
@@ -45,23 +52,36 @@ use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
};
use smithay::wayland::tablet_manager::TabletSeatHandler;
use smithay::wayland::xdg_activation::{
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, 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_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
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_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
use crate::niri::{ClientState, State};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, DndIcon, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
use crate::utils::output_size;
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
@@ -105,41 +125,115 @@ impl SeatHandler for State {
}
delegate_seat!(State);
delegate_cursor_shape!(State);
delegate_tablet_manager!(State);
delegate_pointer_gestures!(State);
delegate_relative_pointer!(State);
delegate_text_input_manager!(State);
impl TabletSeatHandler for State {
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) {
// FIXME: tablet tools should have their own cursors.
self.niri.cursor_manager.set_cursor_image(image);
// FIXME: granular.
self.niri.queue_redraw_all();
}
}
delegate_tablet_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
pointer.current_location(),
&self.niri.pointer_focus,
);
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
// Pointer constraints track pointer focus internally, so make sure it's up to date before
// activating a new one.
self.refresh_pointer_contents();
self.niri.maybe_activate_pointer_constraint();
}
fn cursor_position_hint(
&mut self,
surface: &WlSurface,
pointer: &PointerHandle<Self>,
location: Point<f64, Logical>,
) {
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
constraint.is_some_and(|c| c.is_active())
});
if !is_constraint_active {
return;
}
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
// will change, even though the real pointer focus remains on the Blender surface due to
// the click grab.
//
// Ideally we would just use the constraint surface, but we need its origin. So this is
// more of a hack because pointer contents has the surface origin available.
//
// FIXME: use the constraint surface somehow, don't use pointer contents.
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
return;
};
if surface_under_pointer != surface {
return;
}
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
let target = self
.niri
.output_for_root(&root)
.and_then(|output| self.niri.global_space.output_geometry(output))
.map_or(origin + location, |mut output_geometry| {
// i32 sizes are exclusive, but f64 sizes are inclusive.
output_geometry.size -= (1, 1).into();
(origin + location).constrain(output_geometry.to_f64())
});
pointer.set_location(target);
// Redraw to update the cursor position if it's visible.
if !self.niri.pointer_hidden {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
}
}
delegate_pointer_constraints!(State);
impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
let popup = PopupKind::from(surface.clone());
let popup = PopupKind::InputMethod(surface);
if let Some(output) = self.output_for_popup(&popup) {
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
let wl_surface = popup.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
}
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking ime popup {err:?}");
}
}
fn popup_repositioned(&mut self, surface: PopupSurface) {
let popup = PopupKind::InputMethod(surface);
self.unconstrain_popup(&popup);
}
fn dismiss_popup(&mut self, surface: PopupSurface) {
if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) {
let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
}
}
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
self.niri
.layout
@@ -167,6 +261,10 @@ impl SelectionHandler for State {
let buf = user_data.clone();
thread::spawn(move || {
// Clear O_NONBLOCK, otherwise File::write_all() will stop halfway.
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
warn!("error clearing flags on selection target fd: {err:?}");
}
if let Err(err) = File::from(fd).write_all(&buf) {
warn!("error writing selection: {err:?}");
}
@@ -187,12 +285,61 @@ impl ClientDndGrabHandler for State {
icon: Option<WlSurface>,
_seat: Seat<Self>,
) {
self.niri.dnd_icon = icon;
let offset = if let CursorImageStatus::Surface(ref surface) =
self.niri.cursor_manager.cursor_image()
{
with_states(surface, |states| {
let hotspot = states
.data_map
.get::<CursorImageSurfaceData>()
.unwrap()
.lock()
.unwrap()
.hotspot;
Point::from((-hotspot.x, -hotspot.y))
})
} else {
(0, 0).into()
};
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
// FIXME: more granular
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();
@@ -258,11 +405,15 @@ 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) {
let Some(output) = Output::from_resource(&output) else {
error!("no Output matching WlOutput");
warn!("no Output matching WlOutput");
return;
};
@@ -277,11 +428,11 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
let size = output_size(output);
states.size = Some(Size::from((size.w as u32, size.h as u32)));
});
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
surface.send_configure();
}
@@ -296,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) {
@@ -336,6 +488,7 @@ impl ForeignToplevelHandler for State {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
}
}
@@ -349,12 +502,12 @@ impl ForeignToplevelHandler for State {
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !mapped
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
role.current
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
});
if !has_fullscreen_cap {
return;
}
@@ -364,7 +517,7 @@ impl ForeignToplevelHandler for State {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
.move_to_output(Some(&window), &requested_output, None);
}
}
@@ -382,25 +535,42 @@ impl ForeignToplevelHandler for State {
delegate_foreign_toplevel!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, screencopy: Screencopy) {
if let Err(err) = self
.niri
.render_for_screencopy(&mut self.backend, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
// If with_damage then push it onto the queue for redraw of the output,
// otherwise render it immediately.
if screencopy.with_damage() {
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
trace!("screencopy manager destroyed already");
return;
};
queue.push(screencopy);
} else {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self
.niri
.render_for_screencopy_without_damage(renderer, manager, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
}
});
}
}
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState {
&mut self.niri.screencopy_state
}
}
delegate_screencopy!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
&mut self
.backend
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.drm_lease_state
.as_mut()
.unwrap()
}
fn lease_request(
@@ -471,3 +641,66 @@ impl GammaControlHandler for State {
}
}
delegate_gamma_control!(State);
impl XdgActivationHandler for State {
fn activation_state(&mut self) -> &mut XdgActivationState {
&mut self.niri.activation_state
}
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
// Only tokens that were created while the application has keyboard focus are valid.
let Some((serial, seat)) = data.serial else {
return false;
};
let Some(seat) = Seat::<State>::from_resource(&seat) else {
return false;
};
let keyboard = seat.get_keyboard().unwrap();
keyboard
.last_enter()
.map(|last_enter| serial.is_no_older_than(&last_enter))
.unwrap_or(false)
}
fn request_activation(
&mut self,
token: XdgActivationToken,
token_data: XdgActivationTokenData,
surface: WlSurface,
) {
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
} 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);
impl FractionalScaleHandler for State {}
delegate_fractional_scale!(State);
impl OutputManagementHandler for State {
fn output_management_state(&mut self) -> &mut OutputManagementManagerState {
&mut self.niri.output_management_state
}
fn apply_output_config(&mut self, config: niri_config::Outputs) {
self.niri.config.borrow_mut().outputs = config;
self.reload_output_config();
}
}
delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
delegate_single_pixel_buffer!(State);
+667 -156
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+234
View File
@@ -0,0 +1,234 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
is_moving: false,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for MoveGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
}
} else {
return;
}
}
// The move is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// FIXME: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
// When moving with the left button, right toggles floating, and vice versa.
let toggle_floating_button = if self.start_data.button == 0x110 {
0x111
} 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);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+175
View File
@@ -0,0 +1,175 @@
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct ResizeGrab {
start_data: PointerGrabStartData<State>,
window: Window,
}
impl ResizeGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self { start_data, window }
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for ResizeGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
let delta = event.location - self.start_data.location;
let ongoing = data
.niri
.layout
.interactive_resize_update(&self.window, delta);
if ongoing {
return;
}
}
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+230
View File
@@ -0,0 +1,230 @@
use std::time::Duration;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => None,
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(false, Some(false))
}
};
if let Some(output) = res {
state.niri.queue_redraw(&output);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for SpatialMovementGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
let timestamp = Duration::from_millis(u64::from(event.time));
let delta = event.location - self.last_location;
self.last_location = event.location;
let layout = &mut data.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
layout.workspace_switch_gesture_update(-c.y, timestamp, false)
}
} else {
Some(None)
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_update(-delta.x, timestamp, false)
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_update(-delta.y, timestamp, false)
}
};
if let Some(output) = res {
if let Some(output) = output {
data.niri.queue_redraw(&output);
}
} else {
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+136
View File
@@ -0,0 +1,136 @@
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::niri::State;
pub struct TouchMoveGrab {
start_data: TouchGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
}
impl TouchMoveGrab {
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
}
}
impl TouchGrab<State> for TouchMoveGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
if self.window.alive() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
}
} else {
return;
}
}
// The move is no longer ongoing.
handle.unset_grab(self, data);
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+119
View File
@@ -0,0 +1,119 @@
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::niri::State;
pub struct TouchResizeGrab {
start_data: TouchGrabStartData<State>,
window: Window,
}
impl TouchResizeGrab {
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
Self { start_data, window }
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
}
}
impl TouchGrab<State> for TouchResizeGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
if self.window.alive() {
let delta = event.location - self.start_data.location;
let ongoing = data
.niri
.layout
.interactive_resize_update(&self.window, delta);
if ongoing {
return;
}
}
// The resize is no longer ongoing.
handle.unset_grab(self, data);
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+478 -131
View File
@@ -1,49 +1,118 @@
use std::env;
use std::io::{Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::iter::Peekable;
use std::slice;
use anyhow::{anyhow, bail, Context};
use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
};
use serde_json::json;
use crate::cli::Msg;
use crate::utils::version;
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
format!(
"{} is not set, are you running this within niri?",
niri_ipc::SOCKET_PATH_ENV
)
})?;
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match &msg {
Msg::Version => Request::Version,
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
action: action.clone(),
},
Msg::Workspaces => Request::Workspaces,
Msg::Windows => Request::Windows,
Msg::Layers => Request::Layers,
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
};
let mut buf = serde_json::to_vec(&request).unwrap();
stream
.write_all(&buf)
.context("error writing IPC request")?;
stream
.shutdown(Shutdown::Write)
.context("error closing IPC stream for writing")?;
buf.clear();
stream
.read_to_end(&mut buf)
.context("error reading IPC response")?;
let socket = Socket::connect().context("error connecting to the niri socket")?;
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
let (reply, mut read_event) = socket
.send(request)
.context("error communicating with niri")?;
let response = reply
.map_err(|msg| anyhow!(msg))
.context("niri could not handle the request")?;
let compositor_version = match reply {
Err(_) if !matches!(msg, Msg::Version) => {
// If we got an error, it might be that the CLI is a different version from the running
// niri instance. Request the running instance version to compare and print a message.
Socket::connect()
.and_then(|socket| socket.send(Request::Version))
.ok()
.map(|(reply, _read_event)| reply)
}
_ => None,
};
// Default SIGPIPE so that our prints don't panic on stdout closing.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let response = reply.map_err(|err_msg| {
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request.
// Don't add irrelevant context.
}
}
anyhow!(err_msg).context("niri returned an error")
})?;
match msg {
Msg::RequestError => {
bail!("unexpected response: expected an error, got {response:?}");
}
Msg::Version => {
let Response::Version(compositor_version) = response else {
bail!("unexpected response: expected Version, got {response:?}");
};
let cli_version = version();
if json {
println!(
"{}",
json!({
"compositor": compositor_version,
"cli": cli_version,
})
);
return Ok(());
}
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
println!("Compositor version: {compositor_version}");
println!("CLI version: {cli_version}");
}
Msg::Outputs => {
let Response::Outputs(outputs) = response else {
bail!("unexpected response: expected Outputs, got {response:?}");
@@ -56,95 +125,14 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
return Ok(());
}
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let mut outputs = outputs
.into_values()
.map(|out| (OutputName::from_ipc_output(&out), out))
.collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.compare(&b.0));
for (connector, output) in outputs.into_iter() {
let Output {
name,
make,
model,
physical_size,
modes,
current_mode,
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else {
println!(" Disabled");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
println!(" Physical size: unknown");
}
if let Some(logical) = logical {
let LogicalOutput {
x,
y,
width,
height,
scale,
transform,
} = logical;
println!(" Logical position: {x}, {y}");
println!(" Logical size: {width}x{height}");
println!(" Scale: {scale}");
let transform = match transform {
niri_ipc::Transform::Normal => "normal",
niri_ipc::Transform::_90 => "90° counter-clockwise",
niri_ipc::Transform::_180 => "180°",
niri_ipc::Transform::_270 => "270° counter-clockwise",
niri_ipc::Transform::Flipped => "flipped horizontally",
niri_ipc::Transform::Flipped90 => {
"90° counter-clockwise, flipped horizontally"
}
niri_ipc::Transform::Flipped180 => "flipped vertically",
niri_ipc::Transform::Flipped270 => {
"270° counter-clockwise, flipped horizontally"
}
};
println!(" Transform: {transform}");
}
println!(" Available modes:");
for (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
let is_current = Some(idx) == current_mode;
let qualifier = match (is_current, is_preferred) {
(true, true) => " (current, preferred)",
(true, false) => " (current)",
(false, true) => " (preferred)",
(false, false) => "",
};
println!(" {width}x{height}@{refresh:.3}{qualifier}");
}
for (_name, output) in outputs.into_iter() {
print_output(output)?;
println!();
}
}
@@ -160,29 +148,388 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
}
if let Some(window) = window {
println!("Focused window:");
if let Some(title) = window.title {
println!(" Title: \"{title}\"");
} else {
println!(" Title: (unset)");
}
if let Some(app_id) = window.app_id {
println!(" App ID: \"{app_id}\"");
} else {
println!(" App ID: (unset)");
}
print_window(&window);
} else {
println!("No window is focused.");
}
}
Msg::Windows => {
let Response::Windows(mut windows) = response else {
bail!("unexpected response: expected Windows, got {response:?}");
};
if json {
let windows =
serde_json::to_string(&windows).context("error formatting response")?;
println!("{windows}");
return Ok(());
}
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
for window in windows {
print_window(&window);
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:?}");
};
if json {
let output = serde_json::to_string(&output).context("error formatting response")?;
println!("{output}");
return Ok(());
}
if let Some(output) = output {
print_output(output)?;
} else {
println!("No output is focused.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
Msg::Output { output, .. } => {
let Response::OutputConfigChanged(response) = response else {
bail!("unexpected response: expected OutputConfigChanged, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response == OutputConfigChanged::OutputWasMissing {
println!("Output \"{output}\" is not connected.");
println!("The change will apply when it is connected.");
}
}
Msg::Workspaces => {
let Response::Workspaces(mut response) = response else {
bail!("unexpected response: expected Workspaces, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response.is_empty() {
println!("No workspaces.");
return Ok(());
}
response.sort_by_key(|ws| ws.idx);
response.sort_by(|a, b| a.output.cmp(&b.output));
let mut current_output = if let Some(output) = response[0].output.as_deref() {
println!("Output \"{output}\":");
Some(output)
} else {
println!("No output:");
None
};
for ws in &response {
if ws.output.as_deref() != current_output {
let output = ws.output.as_deref().context(
"invalid response: workspace with no output \
following a workspace with an output",
)?;
current_output = Some(output);
println!("\nOutput \"{output}\":");
}
let is_active = if ws.is_active { " * " } else { " " };
let idx = ws.idx;
let name = if let Some(name) = ws.name.as_deref() {
format!(" \"{name}\"")
} else {
String::new()
};
println!("{is_active}{idx}{name}");
}
}
Msg::KeyboardLayouts => {
let Response::KeyboardLayouts(response) = response else {
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let KeyboardLayouts { names, current_idx } = response;
let current_idx = usize::from(current_idx);
println!("Keyboard layouts:");
for (idx, name) in names.iter().enumerate() {
let is_active = if idx == current_idx { " * " } else { " " };
println!("{is_active}{idx} {name}");
}
}
Msg::EventStream => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
if !json {
println!("Started reading events.");
}
loop {
let event = read_event().context("error reading event from niri")?;
if json {
let event = serde_json::to_string(&event).context("error formatting event")?;
println!("{event}");
continue;
}
match event {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
println!(
"Workspace {workspace_id}: \
active window changed to {active_window_id:?}"
);
}
Event::WindowsChanged { windows } => {
println!("Windows changed: {windows:?}");
}
Event::WindowOpenedOrChanged { window } => {
println!("Window opened or changed: {window:?}");
}
Event::WindowClosed { id } => {
println!("Window closed: {id}");
}
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
}
}
}
}
Ok(())
}
fn print_output(output: Output) -> anyhow::Result<()> {
let Output {
name,
make,
model,
serial,
physical_size,
modes,
current_mode,
vrr_supported,
vrr_enabled,
logical,
} = output;
let serial = serial.as_deref().unwrap_or("Unknown");
println!(r#"Output "{make} {model} {serial}" ({name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else {
println!(" Disabled");
}
if vrr_supported {
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
println!(" Variable refresh rate: supported, {enabled}");
} else {
println!(" Variable refresh rate: not supported");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
println!(" Physical size: unknown");
}
if let Some(logical) = logical {
let LogicalOutput {
x,
y,
width,
height,
scale,
transform,
} = logical;
println!(" Logical position: {x}, {y}");
println!(" Logical size: {width}x{height}");
println!(" Scale: {scale}");
let transform = match transform {
Transform::Normal => "normal",
Transform::_90 => "90° counter-clockwise",
Transform::_180 => "180°",
Transform::_270 => "270° counter-clockwise",
Transform::Flipped => "flipped horizontally",
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
Transform::Flipped180 => "flipped vertically",
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
};
println!(" Transform: {transform}");
}
println!(" Available modes:");
for (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
let is_current = Some(idx) == current_mode;
let qualifier = match (is_current, is_preferred) {
(true, true) => " (current, preferred)",
(true, false) => " (current)",
(false, true) => " (preferred)",
(false, false) => "",
};
println!(" {width}x{height}@{refresh:.3}{qualifier}");
}
Ok(())
}
fn print_window(window: &Window) {
let focused = if window.is_focused { " (focused)" } else { "" };
println!("Window ID {}:{focused}", window.id);
if let Some(title) = &window.title {
println!(" Title: \"{title}\"");
} else {
println!(" Title: (unset)");
}
if let Some(app_id) = &window.app_id {
println!(" App ID: \"{app_id}\"");
} else {
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 {
println!(" Workspace ID: (none)");
}
}
+507 -36
View File
@@ -1,32 +1,60 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::{env, io, process};
use anyhow::Context;
use async_channel::{Receiver, Sender, TrySendError};
use calloop::futures::Scheduler;
use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{Request, Response};
use smithay::desktop::Window;
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
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::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use crate::backend::IpcOutputMap;
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
use crate::utils::{version, with_toplevel_role};
use crate::window::Mapped;
// If an event stream client fails to read events fast enough that we accumulate more than this
// number in our buffer, we drop that event stream client.
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
pub struct IpcServer {
pub socket_path: PathBuf,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
scheduler: Scheduler<()>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
ipc_focused_window: Arc<Mutex<Option<Window>>>,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
struct EventStreamClient {
events: Receiver<Event>,
disconnect: Receiver<()>,
write: Box<dyn AsyncWrite + Unpin>,
}
struct EventStreamSender {
events: Sender<Event>,
disconnect: Sender<()>,
}
impl IpcServer {
@@ -58,7 +86,34 @@ impl IpcServer {
})
.unwrap();
Ok(Self { socket_path })
Ok(Self {
socket_path,
event_streams: Rc::new(RefCell::new(Vec::new())),
event_stream_state: Rc::new(RefCell::new(EventStreamState::default())),
})
}
fn send_event(&self, event: Event) {
let mut streams = self.event_streams.borrow_mut();
let mut to_remove = Vec::new();
for (idx, stream) in streams.iter_mut().enumerate() {
match stream.events.try_send(event.clone()) {
Ok(()) => (),
Err(TrySendError::Closed(_)) => to_remove.push(idx),
Err(TrySendError::Full(_)) => {
warn!(
"disconnecting IPC event stream client \
because it is reading events too slowly"
);
to_remove.push(idx);
}
}
}
for idx in to_remove.into_iter().rev() {
let stream = streams.swap_remove(idx);
let _ = stream.disconnect.send_blocking(());
}
}
}
@@ -88,10 +143,14 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
}
};
let ipc_server = state.niri.ipc_server.as_ref().unwrap();
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
scheduler: state.niri.scheduler.clone(),
ipc_outputs: state.backend.ipc_outputs(),
ipc_focused_window: state.niri.ipc_focused_window.clone(),
event_streams: ipc_server.event_streams.clone(),
event_stream_state: ipc_server.event_stream_state.clone(),
};
let future = async move {
@@ -104,7 +163,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
}
}
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
@@ -114,53 +173,465 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
.await
.context("error reading request")?;
let reply = process(&ctx, &buf).map_err(|err| {
warn!("error processing IPC request: {err:?}");
err.to_string()
});
let request = serde_json::from_str(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
}
Ok(())
}
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let response = match request {
Request::ReturnError => return Err(String::from("example compositor error")),
Request::Version => Response::Version(version()),
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
Response::Outputs(ipc_outputs)
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
Response::Outputs(outputs.collect())
}
Request::Workspaces => {
let state = ctx.event_stream_state.borrow();
let workspaces = state.workspaces.workspaces.values().cloned().collect();
Response::Workspaces(workspaces)
}
Request::Windows => {
let state = ctx.event_stream_state.borrow();
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();
let layout = layout.expect("keyboard layouts should be set at startup");
Response::KeyboardLayouts(layout)
}
Request::FocusedWindow => {
let window = ctx.ipc_focused_window.lock().unwrap().clone();
let window = window.map(|window| {
let wl_surface = window.toplevel().expect("no X11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
title: role.title.clone(),
app_id: role.app_id.clone(),
}
})
});
let state = ctx.event_stream_state.borrow();
let windows = &state.windows.windows;
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::Action(action) => {
let (tx, rx) = async_channel::bounded(1);
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action);
// 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(());
});
// Wait until the action has been processed before returning. This is important for a
// few actions, for instance for DoScreenTransition this wait ensures that the screen
// contents were sampled into the texture.
let _ = rx.recv().await;
Response::Handled
}
Request::Output { output, action } => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.values()
.any(|o| OutputName::from_ipc_output(o).matches(&output));
let response = if found {
OutputConfigChanged::Applied
} else {
OutputConfigChanged::OutputWasMissing
};
drop(ipc_outputs);
ctx.event_loop.insert_idle(move |state| {
state.apply_transient_output_config(&output, action);
});
Response::OutputConfigChanged(response)
}
Request::FocusedOutput => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let active_output = state
.niri
.layout
.active_output()
.map(|output| output.name());
let output = active_output.and_then(|active_output| {
state
.backend
.ipc_outputs()
.lock()
.unwrap()
.values()
.find(|o| o.name == active_output)
.cloned()
});
let _ = tx.send_blocking(output);
});
let result = rx.recv().await;
let output = result.map_err(|_| String::from("error getting active output info"))?;
Response::FocusedOutput(output)
}
Request::EventStream => Response::Handled,
};
Ok(response)
}
async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> {
let EventStreamClient {
events,
disconnect,
mut write,
} = client;
while let Ok(event) = events.recv().await {
let mut buf = serde_json::to_vec(&event).context("error formatting event")?;
buf.push(b'\n');
let res = select_biased! {
_ = disconnect.recv().fuse() => return Ok(()),
res = write.write_all(&buf).fuse() => res,
};
match res {
Ok(()) => (),
// Normal client disconnection.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
res @ Err(_) => res.context("error writing event")?,
}
}
Ok(())
}
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
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(),
})
}
impl State {
pub fn ipc_keyboard_layouts_changed(&mut self) {
let keyboard = self.niri.seat.get_keyboard().unwrap();
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
let layouts = xkb.layouts();
KeyboardLayouts {
names: layouts
.map(|layout| xkb.layout_name(layout).to_owned())
.collect(),
current_idx: xkb.active_layout().0 as u8,
}
});
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.keyboard_layouts;
let event = Event::KeyboardLayoutsChanged { keyboard_layouts };
state.apply(event.clone());
server.send_event(event);
}
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
let keyboard = self.niri.seat.get_keyboard().unwrap();
let idx = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
xkb.active_layout().0 as u8
});
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.keyboard_layouts;
if state.keyboard_layouts.as_ref().unwrap().current_idx == idx {
return;
}
let event = Event::KeyboardLayoutSwitched { idx };
state.apply(event.clone());
server.send_event(event);
}
pub fn ipc_refresh_layout(&mut self) {
self.ipc_refresh_workspaces();
self.ipc_refresh_windows();
}
fn ipc_refresh_workspaces(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let _span = tracy_client::span!("State::ipc_refresh_workspaces");
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.workspaces;
let mut events = Vec::new();
let layout = &self.niri.layout;
let focused_ws_id = layout.active_workspace().map(|ws| ws.id().get());
// Check for workspace changes.
let mut seen = HashSet::new();
let mut need_workspaces_changed = false;
for (mon, ws_idx, ws) in layout.workspaces() {
let id = ws.id().get();
seen.insert(id);
let Some(ipc_ws) = state.workspaces.get(&id) else {
// A new workspace was added.
need_workspaces_changed = true;
break;
};
// Check for any changes that we can't signal as individual events.
let output_name = mon.map(|mon| mon.output_name());
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|| ipc_ws.name.as_ref() != ws.name()
|| ipc_ws.output.as_ref() != output_name
{
need_workspaces_changed = true;
break;
}
let active_window_id = ws.active_window().map(|win| win.id().get());
if ipc_ws.active_window_id != active_window_id {
events.push(Event::WorkspaceActiveWindowChanged {
workspace_id: id,
active_window_id,
});
}
// Check if this workspace became focused.
let is_focused = Some(id) == focused_ws_id;
if is_focused && !ipc_ws.is_focused {
events.push(Event::WorkspaceActivated { id, focused: true });
continue;
}
// Check if this workspace became active.
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 });
}
}
// Check if any workspaces were removed.
if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) {
need_workspaces_changed = true;
}
if need_workspaces_changed {
events.clear();
let workspaces = layout
.workspaces()
.map(|(mon, ws_idx, ws)| {
let id = ws.id().get();
Workspace {
id,
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.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()),
}
})
.collect();
events.push(Event::WorkspacesChanged { workspaces });
}
for event in events {
state.apply(event.clone());
server.send_event(event);
}
}
fn ipc_refresh_windows(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let _span = tracy_client::span!("State::ipc_refresh_windows");
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.windows;
let mut events = Vec::new();
let layout = &self.niri.layout;
// Check for window changes.
let mut seen = HashSet::new();
let mut focused_id = None;
layout.with_windows(|mapped, _, ws_id| {
let id = mapped.id().get();
seen.insert(id);
if mapped.is_focused() {
focused_id = Some(id);
}
let Some(ipc_win) = state.windows.get(&id) else {
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
};
let workspace_id = ws_id.map(|id| id.get());
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
});
if changed {
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
}
if mapped.is_focused() && !ipc_win.is_focused {
events.push(Event::WindowFocusChanged { id: Some(id) });
}
});
// Check for closed windows.
let mut ipc_focused_id = None;
for (id, ipc_win) in &state.windows {
if !seen.contains(id) {
events.push(Event::WindowClosed { id: *id });
}
if ipc_win.is_focused {
ipc_focused_id = Some(id);
}
}
// Extra check for focus becoming None, since the checks above only work for focus becoming
// a different window.
if focused_id.is_none() && ipc_focused_id.is_some() {
events.push(Event::WindowFocusChanged { id: None });
}
for event in events {
state.apply(event.clone());
server.send_event(event);
}
}
}
+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
}
+275
View File
@@ -0,0 +1,275 @@
use std::collections::HashMap;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{Blocker, BlockerState};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer<GlesTexture>,
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer<GlesTexture>,
/// Where the window should be blocked out from.
block_out_from: Option<BlockOutFrom>,
/// Size of the window geometry.
geo_size: Size<f64, Logical>,
/// Position in the workspace.
pos: Point<f64, Logical>,
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
/// The closing animation.
anim_state: AnimationState,
/// Random seed for the shader.
random_seed: f32,
}
niri_render_elements! {
ClosingWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
#[derive(Debug)]
enum AnimationState {
Waiting {
/// Blocker for a transaction before starting the animation.
blocker: TransactionBlocker,
anim: Animation,
},
Animating(Animation),
}
impl AnimationState {
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
if blocker.state() == BlockerState::Pending {
Self::Waiting { blocker, anim }
} else {
// This actually doesn't normally happen because the window is removed only after the
// closing animation is created. Though, it does happen with disable-transactions debug
// flag.
Self::Animating(anim)
}
}
}
impl ClosingWindow {
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: Scale<f64>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
blocker: TransactionBlocker,
anim: Animation,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
let mut render_to_texture = |elements: Vec<E>| -> anyhow::Result<_> {
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
)
.context("error rendering to texture")?;
let buffer = TextureBuffer::from_texture(
renderer,
texture,
scale,
Transform::Normal,
Vec::new(),
);
let offset = geo.loc.to_f64().to_logical(scale);
Ok((buffer, offset))
};
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
})
}
pub fn advance_animations(&mut self) {
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let anim = anim.restarted(0., 1., 0.);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(_anim) => (),
}
}
pub fn are_animations_ongoing(&self) -> bool {
match &self.anim_state {
AnimationState::Waiting { .. } => true,
AnimationState::Animating(anim) => !anim.is_done(),
}
}
pub fn render(
&self,
renderer: &mut GlesRenderer,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.buffer, self.buffer_offset)
};
let anim = match &self.anim_state {
AnimationState::Waiting { .. } => {
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
1.,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), 1.);
let mut location = self.pos + offset;
location.x -= view_rect.loc.x;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
);
return elem.into();
}
AnimationState::Animating(anim) => anim,
};
let progress = anim.value();
let clamped_progress = anim.clamped_value().clamp(0., 1.);
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
// Round to physical pixels relative to the view position. This is similar to what
// happens when rendering normal windows.
let relative = self.pos - view_rect.loc;
let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale);
let geo_loc = Vec2::new(pos.x as f32, pos.y as f32);
let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = self.buffer.texture_scale();
let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = self.buffer.texture().size();
let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return ShaderRenderElement::new(
ProgramType::Close,
view_rect.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
Kind::Unspecified,
)
.with_location(Point::from((0., 0.)))
.into();
}
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
1. - clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = self.geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
((1. - clamped_progress) / 5. + 0.8).max(0.),
);
let mut location = self.pos + offset;
location.x -= view_rect.loc.x;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
);
elem.into()
}
}
File diff suppressed because it is too large Load Diff
+189 -88
View File
@@ -1,30 +1,31 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::GradientRelativeTo;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::niri_render_elements;
use crate::render_helpers::gradient::GradientRenderElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
sizes: [Size<i32, Logical>; 4],
full_size: Size<i32, Logical>,
is_active: bool,
buffers: [SolidColorBuffer; 8],
locations: [Point<f64, Logical>; 8],
sizes: [Size<f64, Logical>; 8],
borders: [BorderRenderElement; 8],
full_size: Size<f64, Logical>,
is_border: bool,
use_border_shader: bool,
config: niri_config::FocusRing,
}
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = GradientRenderElement,
Gradient = BorderRenderElement,
}
}
@@ -34,9 +35,10 @@ impl FocusRing {
buffers: Default::default(),
locations: Default::default(),
sizes: Default::default(),
borders: Default::default(),
full_size: Default::default(),
is_active: false,
is_border: false,
use_border_shader: false,
config,
}
}
@@ -45,117 +47,216 @@ impl FocusRing {
self.config = config;
}
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
if is_border {
self.sizes[0] = Size::from((win_size.w + width * 2, width));
self.sizes[1] = Size::from((win_size.w + width * 2, width));
self.sizes[2] = Size::from((width, win_size.h));
self.sizes[3] = Size::from((width, win_size.h));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
pub fn update_shaders(&mut self) {
for elem in &mut self.borders {
elem.damage_all();
}
self.is_border = is_border;
}
pub fn set_active(&mut self, is_active: bool) {
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.active_color.into()
self.config.active_color
} else {
self.config.inactive_color.into()
self.config.inactive_color
};
for buf in &mut self.buffers {
buf.set_color(color);
buf.set_color(color.to_array_premul());
}
self.is_active = is_active;
}
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.config.off {
return rv.into_iter();
}
let gradient = if self.is_active {
let gradient = if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
let elem = gradient.and_then(|gradient| {
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
GradientRenderElement::new(
renderer,
scale,
Rectangle::from_loc_and_size(location, size),
gradient_area,
gradient.from.into(),
gradient.to.into(),
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let 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,
};
let rounded_corner_border_width = if self.is_border {
// HACK: increase the border width used for the inner rounded corners a tiny bit to
// reduce background bleed.
width as f32 + 0.5
} else {
0.
};
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size and border width are rounded to physical pixels before being passed to this
// function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
if is_border {
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
let top_right = f64::min(
self.full_size.w - top_left,
f64::max(width, ceil(f64::from(radius.top_right))),
);
let bottom_left = f64::min(
self.full_size.h - top_left,
f64::max(width, ceil(f64::from(radius.bottom_left))),
);
let bottom_right = f64::min(
self.full_size.h - top_right,
f64::min(
self.full_size.w - bottom_left,
f64::max(width, ceil(f64::from(radius.bottom_right))),
),
);
// Top edge.
self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width));
self.locations[0] = Point::from((-width + top_left, -width));
// Bottom edge.
self.sizes[1] =
Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width));
self.locations[1] = Point::from((-width + bottom_left, win_size.h));
// Left edge.
self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left));
self.locations[2] = Point::from((-width, -width + top_left));
// Right edge.
self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right));
self.locations[3] = Point::from((win_size.w, -width + top_right));
// Top-left corner.
self.sizes[4] = Size::from((top_left, top_left));
self.locations[4] = Point::from((-width, -width));
// Top-right corner.
self.sizes[5] = Size::from((top_right, top_right));
self.locations[5] = Point::from((win_size.w + width - top_right, -width));
// Bottom-right corner.
self.sizes[6] = Size::from((bottom_right, bottom_right));
self.locations[6] = Point::from((
win_size.w + width - bottom_right,
win_size.h + width - bottom_right,
));
// Bottom-left corner.
self.sizes[7] = Size::from((bottom_left, bottom_left));
self.locations[7] = Point::from((-width, win_size.h + width - bottom_left));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
)
.map(Into::into)
});
Rectangle::new(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
let elem = elem.unwrap_or_else(|| {
SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
self.borders[0].update(
self.sizes[0],
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::new(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
self.is_border = is_border;
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 8>::new();
if self.config.off {
return rv.into_iter();
}
let border_width = -self.locations[0].y;
// If drawing as a border with width = 0, then there's nothing to draw.
if self.is_border && border_width == 0. {
return rv.into_iter();
}
let has_border_shader = BorderRenderElement::has_shader(renderer);
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
};
rv.push(elem);
};
if self.is_border {
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
push(buf, location + loc, size);
for ((buf, border), loc) in zip(zip(&self.buffers, &self.borders), self.locations) {
push(buf, border, location + loc);
}
} else {
push(
&self.buffers[0],
&self.borders[0],
location + self.locations[0],
self.sizes[0],
);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.config.width.into()
pub fn width(&self) -> f64 {
self.config.width.0
}
pub fn is_off(&self) -> bool {
+61
View File
@@ -0,0 +1,61 @@
use niri_config::{CornerRadius, FloatOrInt};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct InsertHintElement {
inner: FocusRing,
}
pub type InsertHintRenderElement = FocusRingRenderElement;
impl InsertHintElement {
pub fn new(config: niri_config::InsertHint) -> Self {
Self {
inner: FocusRing::new(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
}),
}
}
pub fn update_config(&mut self, config: niri_config::InsertHint) {
self.inner.update_config(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
});
}
pub fn update_shaders(&mut self) {
self.inner.update_shaders();
}
pub fn update_render_elements(
&mut self,
size: Size<f64, Logical>,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
self.inner
.update_render_elements(size, true, false, view_rect, radius, scale);
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
self.inner.render(renderer, location)
}
}
+4772 -676
View File
File diff suppressed because it is too large Load Diff
+576 -338
View File
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
use std::collections::HashMap;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
impl OpenAnimation {
pub fn new(anim: Animation) -> Self {
Self {
anim,
random_seed: fastrand::f32(),
}
}
pub fn advance_animations(&mut self) {}
pub fn is_done(&self) -> bool {
self.anim.is_done()
}
// We can't depend on view_rect here, because the result of window opening can be snapshot and
// then rendered elsewhere.
pub fn render(
&self,
renderer: &mut GlesRenderer,
elements: &[impl RenderElement<GlesRenderer>],
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
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);
target_size.w = f64::max(area.size.w + 1000., target_size.w);
target_size.h = f64::max(area.size.h + 1000., target_size.h);
let diff = (target_size.to_point() - area.size.to_point()).downscale(2.);
let diff = diff.to_physical_precise_round(scale).to_logical(scale);
area.loc -= diff;
area.size += diff.upscale(2.).to_size();
let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32);
let area_size = Vec2::new(area.size.w as f32, area.size.h as f32);
let geo_loc = Vec2::new(location.x as f32, location.y as f32);
let geo_size = Vec2::new(geo_size.w as f32, geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = Vec2::new(scale.x as f32, scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return Ok(ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc)
.into());
}
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
(location + offset).to_physical_precise_round(scale),
Relocate::Relative,
);
Ok(elem.into())
}
}
File diff suppressed because it is too large Load Diff
+773 -172
View File
File diff suppressed because it is too large Load Diff
+1387 -1876
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -11,13 +11,12 @@ 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;
pub mod render_helpers;
pub mod rubber_band;
pub mod scroll_tracker;
pub mod swipe_tracker;
pub mod ui;
pub mod utils;
pub mod window;
@@ -29,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;
+108 -73
View File
@@ -11,24 +11,32 @@ use std::{env, mem};
use clap::Parser;
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
spawn, store_and_increase_nofile_rlimit, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use niri_ipc::socket::SOCKET_PATH_ENV;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
#[cfg(feature = "profile-with-tracy-allocations")]
#[global_allocator]
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
if env::var_os("RUST_BACKTRACE").is_none() {
@@ -50,7 +58,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
@@ -78,8 +86,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("XDG_SESSION_TYPE", "wayland");
}
let _client = tracy_client::Client::start();
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
@@ -87,9 +93,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
let path = config
.or_else(default_config_path)
.expect("error getting config path");
tracy_client::Client::start();
let (path, _, _) = config_path(config);
Config::load(&path)?;
info!("config is valid");
return Ok(());
@@ -102,71 +108,66 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Avoid starting Tracy for the `niri msg` code path since starting/stopping Tracy is a bit
// slow.
tracy_client::Client::start();
info!("starting version {}", &version());
// Load the config.
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
let (path, watch_path, create_default) = config_path(cli.config);
env::remove_var("NIRI_CONFIG");
if create_default {
let default_parent = path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
match fs::create_dir_all(default_parent) {
Ok(()) => {
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &path),
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
Err(err) => {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
}
}
Some(default_path)
});
}
let mut config_errored = false;
let mut config = path
.as_deref()
.and_then(|path| match Config::load(path) {
Ok(config) => Some(config),
Err(err) => {
warn!("{err:?}");
config_errored = true;
None
}
let mut config = Config::load(&path)
.map_err(|err| {
warn!("{err:?}");
config_errored = true;
})
.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);
store_and_increase_nofile_rlimit();
// Create the compositor.
let mut event_loop = EventLoop::try_new().unwrap();
let display = Display::new().unwrap();
@@ -175,6 +176,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
false,
)
.unwrap();
@@ -188,7 +190,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
@@ -208,37 +210,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "dbus")]
dbus::DBusServers::start(&mut state, cli.session);
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
// Send ready notification to the NOTIFY_FD file descriptor.
if let Err(err) = notify_fd() {
warn!("error notifying fd: {err:?}");
// Send ready notification to the NOTIFY_FD file descriptor.
if let Err(err) = notify_fd() {
warn!("error notifying fd: {err:?}");
}
}
// Set up config file watcher.
let _watcher = if let Some(path) = path.clone() {
let _watcher = {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
let watcher = Watcher::new(watch_path.clone(), tx);
event_loop
.handle()
.insert_source(rx, move |event, _, state| match event {
calloop::channel::Event::Msg(()) => state.reload_config(path.clone()),
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
calloop::channel::Event::Closed => (),
})
.unwrap();
Some(watcher)
} else {
None
watcher
};
// 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.
@@ -261,7 +263,7 @@ fn import_environment() {
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
SOCKET_PATH_ENV,
]
.join(" ");
@@ -306,6 +308,12 @@ fn import_environment() {
}
}
fn env_config_path() -> Option<PathBuf> {
env::var_os("NIRI_CONFIG")
.filter(|x| !x.is_empty())
.map(PathBuf::from)
}
fn default_config_path() -> Option<PathBuf> {
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
warn!("error retrieving home directory");
@@ -317,6 +325,33 @@ fn default_config_path() -> Option<PathBuf> {
Some(path)
}
fn system_config_path() -> PathBuf {
PathBuf::from("/etc/niri/config.kdl")
}
/// Resolves and returns the config path to load, the config path to watch, and whether to create
/// the default config at the path to load.
fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
if let Some(explicit) = cli_path.or_else(env_config_path) {
return (explicit.clone(), explicit, false);
}
let system_path = system_config_path();
if let Some(path) = default_config_path() {
if path.exists() {
return (path.clone(), path, true);
}
if system_path.exists() {
(system_path, path, false)
} else {
(path.clone(), path, true)
}
} else {
(system_path.clone(), system_path, false)
}
}
fn notify_fd() -> anyhow::Result<()> {
let fd = match env::var("NOTIFY_FD") {
Ok(notify_fd) => notify_fd.parse()?,
+2108 -554
View File
File diff suppressed because it is too large Load Diff
+11 -27
View File
@@ -11,10 +11,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
@@ -22,6 +19,7 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
use crate::utils::with_toplevel_role;
const VERSION: u32 = 3;
@@ -95,38 +93,24 @@ pub fn refresh(state: &mut State) {
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|mapped, output| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
state.niri.layout.with_windows(|mapped, output, _| {
let toplevel = mapped.toplevel();
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
refresh_toplevel(protocol_state, wl_surface, role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
let toplevel = window.toplevel().expect("no X11 support");
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
});
}
}
-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) => (),
+4
View File
@@ -1,3 +1,7 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
pub mod raw;
+93
View File
@@ -0,0 +1,93 @@
use mutter_x11_interop::MutterX11Interop;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use super::raw::mutter_x11_interop::v1::server::mutter_x11_interop;
const VERSION: u32 = 1;
pub struct MutterX11InteropManagerState {}
pub struct MutterX11InteropManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait MutterX11InteropHandler {}
impl MutterX11InteropManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = MutterX11InteropManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, MutterX11Interop, _>(VERSION, global_data);
Self {}
}
}
impl<D> GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData, D>
for MutterX11InteropManagerState
where
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<MutterX11Interop>,
_manager_state: &MutterX11InteropManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &MutterX11InteropManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<MutterX11Interop, (), D> for MutterX11InteropManagerState
where
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_resource: &MutterX11Interop,
request: <MutterX11Interop as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
mutter_x11_interop::Request::Destroy => (),
mutter_x11_interop::Request::SetX11Parent { .. } => (),
}
}
}
#[macro_export]
macro_rules! delegate_mutter_x11_interop {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: $crate::protocols::mutter_x11_interop::MutterX11InteropManagerGlobalData
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: ()
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
};
}
+897
View File
@@ -0,0 +1,897 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::iter::zip;
use std::mem;
use niri_config::{FloatOrInt, OutputName, Vrr};
use niri_ipc::Transform;
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
zwlr_output_manager_v1, zwlr_output_mode_v1,
};
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::Transform as WlTransform;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
};
use zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1;
use zwlr_output_configuration_v1::ZwlrOutputConfigurationV1;
use zwlr_output_head_v1::{AdaptiveSyncState, ZwlrOutputHeadV1};
use zwlr_output_manager_v1::ZwlrOutputManagerV1;
use zwlr_output_mode_v1::ZwlrOutputModeV1;
use crate::backend::OutputId;
use crate::niri::State;
use crate::utils::ipc_transform_to_smithay;
const VERSION: u32 = 4;
#[derive(Debug)]
struct ClientData {
heads: HashMap<OutputId, (ZwlrOutputHeadV1, Vec<ZwlrOutputModeV1>)>,
confs: HashMap<ZwlrOutputConfigurationV1, OutputConfigurationState>,
manager: ZwlrOutputManagerV1,
}
pub struct OutputManagementManagerState {
display: DisplayHandle,
serial: u32,
clients: HashMap<ClientId, ClientData>,
current_state: HashMap<OutputId, niri_ipc::Output>,
current_config: niri_config::Outputs,
}
pub struct OutputManagementManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait OutputManagementHandler {
fn output_management_state(&mut self) -> &mut OutputManagementManagerState;
fn apply_output_config(&mut self, config: niri_config::Outputs);
}
#[derive(Debug)]
enum OutputConfigurationState {
Ongoing(HashMap<OutputId, niri_config::Output>),
Finished,
}
pub enum OutputConfigurationHeadState {
Cancelled,
Ok(OutputId, ZwlrOutputConfigurationV1),
}
impl OutputManagementManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = OutputManagementManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrOutputManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
clients: HashMap::new(),
serial: 0,
current_state: HashMap::new(),
current_config: Default::default(),
}
}
pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) {
self.current_config = new_config;
}
pub fn notify_changes(&mut self, new_state: HashMap<OutputId, niri_ipc::Output>) {
let mut changed = false; /* most likely to end up true */
for (output, conf) in new_state.iter() {
if let Some(old) = self.current_state.get(output) {
if old.vrr_enabled != conf.vrr_enabled {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
head.adaptive_sync(match conf.vrr_enabled {
true => AdaptiveSyncState::Enabled,
false => AdaptiveSyncState::Disabled,
});
}
}
}
}
// TTY outputs can't change modes I think, however, winit and virtual outputs can.
let modes_changed = old.modes != conf.modes;
if modes_changed {
changed = true;
if old.modes.len() != conf.modes.len() {
error!("output's old mode count doesn't match new modes");
} else {
for client in self.clients.values() {
if let Some((_, modes)) = client.heads.get(output) {
for (wl_mode, mode) in zip(modes, &conf.modes) {
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
wl_mode.refresh(refresh_rate);
}
}
}
}
}
}
match (old.current_mode, conf.current_mode) {
(Some(old_index), Some(new_index)) => {
if old.modes.len() == conf.modes.len()
&& (modes_changed || old_index != new_index)
{
changed = true;
for client in self.clients.values() {
if let Some((head, modes)) = client.heads.get(output) {
if let Some(new_mode) = modes.get(new_index) {
head.current_mode(new_mode);
} else {
error!(
"output new mode doesnt exist for the client's output"
);
}
}
}
}
}
(Some(_), None) => {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
head.enabled(0);
}
}
}
(None, Some(new_index)) => {
if old.modes.len() == conf.modes.len() {
changed = true;
for client in self.clients.values() {
if let Some((head, modes)) = client.heads.get(output) {
head.enabled(1);
if let Some(mode) = modes.get(new_index) {
head.current_mode(mode);
} else {
error!(
"output new mode doesnt exist for the client's output"
);
}
}
}
}
}
(None, None) => {}
}
match (old.logical, conf.logical) {
(Some(old_logical), Some(new_logical)) => {
if old_logical != new_logical {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
if old_logical.x != new_logical.x
|| old_logical.y != new_logical.y
{
head.position(new_logical.x, new_logical.y);
}
if old_logical.scale != new_logical.scale {
head.scale(new_logical.scale);
}
if old_logical.transform != new_logical.transform {
head.transform(
ipc_transform_to_smithay(new_logical.transform).into(),
);
}
}
}
}
}
(None, Some(new_logical)) => {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
// head enable in the mode diff check
head.position(new_logical.x, new_logical.y);
head.transform(
ipc_transform_to_smithay(new_logical.transform).into(),
);
head.scale(new_logical.scale);
}
}
}
(Some(_), None) => {
// heads disabled in the mode diff check
}
(None, None) => {}
}
} else {
changed = true;
notify_new_head(self, output, conf);
}
}
for (old, _) in self.current_state.iter() {
if !new_state.contains_key(old) {
changed = true;
notify_removed_head(&mut self.clients, old);
}
}
if changed {
self.current_state = new_state;
self.serial += 1;
for data in self.clients.values() {
data.manager.done(self.serial);
for conf in data.confs.keys() {
conf.cancelled();
}
}
}
}
}
impl<D> GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData, D>
for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn bind(
state: &mut D,
display: &DisplayHandle,
client: &Client,
manager: New<ZwlrOutputManagerV1>,
_manager_state: &OutputManagementManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(manager, ());
let g_state = state.output_management_state();
let mut client_data = ClientData {
heads: HashMap::new(),
confs: HashMap::new(),
manager: manager.clone(),
};
for (output, conf) in &g_state.current_state {
send_new_head::<D>(display, client, &mut client_data, *output, conf);
}
g_state.clients.insert(client.id(), client_data);
manager.done(g_state.serial);
}
fn can_view(client: Client, global_data: &OutputManagementManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrOutputManagerV1, (), D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
_manager: &ZwlrOutputManagerV1,
request: zwlr_output_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => {
let g_state = state.output_management_state();
let conf = data_init.init(id, serial);
if let Some(client_data) = g_state.clients.get_mut(&client.id()) {
if serial != g_state.serial {
conf.cancelled();
}
let state = OutputConfigurationState::Ongoing(HashMap::new());
client_data.confs.insert(conf, state);
} else {
error!("CreateConfiguration: missing client data");
}
}
zwlr_output_manager_v1::Request::Stop => {
if let Some(c) = state.output_management_state().clients.remove(&client.id()) {
c.manager.finished()
}
}
_ => unreachable!(),
}
}
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputManagerV1, _data: &()) {
state.output_management_state().clients.remove(&client);
}
}
impl<D> Dispatch<ZwlrOutputConfigurationV1, u32, D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
conf: &ZwlrOutputConfigurationV1,
request: zwlr_output_configuration_v1::Request,
serial: &u32,
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let g_state = state.output_management_state();
let outdated = *serial != g_state.serial;
if outdated {
debug!("OutputConfiguration: request from an outdated configuration");
}
let new_config = g_state
.clients
.get_mut(&client.id())
.and_then(|data| data.confs.get_mut(conf));
if new_config.is_none() {
error!("OutputConfiguration: request from unknown configuration object");
}
match request {
zwlr_output_configuration_v1::Request::EnableHead { id, head } => {
let Some(output) = head.data::<OutputId>() else {
error!("EnableHead: Missing attached output");
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
if outdated {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
}
let Some(new_config) = new_config else {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(current_config) = g_state.current_state.get(output) else {
error!("EnableHead: output missing from current config");
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
match new_config.entry(*output) {
Entry::Occupied(_) => {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
"head has been already configured",
);
return;
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = false;
entry.insert(config);
}
};
data_init.init(id, OutputConfigurationHeadState::Ok(*output, conf.clone()));
}
zwlr_output_configuration_v1::Request::DisableHead { head } => {
if outdated {
return;
}
let Some(output) = head.data::<OutputId>() else {
error!("DisableHead: missing attached output head name");
return;
};
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(current_config) = g_state.current_state.get(output) else {
error!("EnableHead: output missing from current config");
return;
};
match new_config.entry(*output) {
Entry::Occupied(_) => {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
"head has been already configured",
);
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = true;
entry.insert(config);
}
};
}
zwlr_output_configuration_v1::Request::Apply => {
if outdated {
conf.cancelled();
return;
}
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) =
mem::replace(new_config, OutputConfigurationState::Finished)
else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let any_enabled = new_config.values().any(|c| !c.off);
if !any_enabled {
conf.failed();
return;
}
state.apply_output_config(new_config.into_values().collect());
// FIXME: verify that it had been applied successfully (which may be difficult).
conf.succeeded();
}
zwlr_output_configuration_v1::Request::Test => {
if outdated {
conf.cancelled();
return;
}
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) =
mem::replace(new_config, OutputConfigurationState::Finished)
else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let any_enabled = new_config.values().any(|c| !c.off);
if !any_enabled {
conf.failed();
return;
}
// FIXME: actually test the configuration with TTY.
conf.succeeded()
}
zwlr_output_configuration_v1::Request::Destroy => {
g_state
.clients
.get_mut(&client.id())
.map(|d| d.confs.remove(conf));
}
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState, D>
for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
conf_head: &ZwlrOutputConfigurationHeadV1,
request: zwlr_output_configuration_head_v1::Request,
data: &OutputConfigurationHeadState,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let g_state = state.output_management_state();
let Some(client_data) = g_state.clients.get_mut(&client.id()) else {
error!("ConfigurationHead: missing client data");
return;
};
let OutputConfigurationHeadState::Ok(output_id, conf) = data else {
warn!("ConfigurationHead: request sent to a cancelled head");
return;
};
let Some(serial) = conf.data::<u32>() else {
error!("ConfigurationHead: missing serial");
return;
};
if *serial != g_state.serial {
warn!("ConfigurationHead: request sent to an outdated");
return;
}
let Some(new_config) = client_data.confs.get_mut(conf) else {
error!("ConfigurationHead: unknown configuration");
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(new_config) = new_config.get_mut(output_id) else {
error!("ConfigurationHead: config missing from enabled heads");
return;
};
match request {
zwlr_output_configuration_head_v1::Request::SetMode { mode } => {
let index = match client_data
.heads
.get(output_id)
.map(|(_, mods)| mods.iter().position(|m| m.id() == mode.id()))
{
Some(Some(index)) => index,
_ => {
warn!("SetMode: failed to find requested mode");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidMode,
"failed to find requested mode",
);
return;
}
};
let Some(current_config) = g_state.current_state.get(output_id) else {
warn!("SetMode: output missing from the current config");
return;
};
let Some(mode) = current_config.modes.get(index) else {
error!("SetMode: requested mode is out of range");
return;
};
new_config.mode = Some(niri_ipc::ConfiguredMode {
width: mode.width,
height: mode.height,
refresh: Some(mode.refresh_rate as f64 / 1000.),
});
}
zwlr_output_configuration_head_v1::Request::SetCustomMode {
width,
height,
refresh,
} => {
// FIXME: Support custom mode
let (width, height, refresh): (u16, u16, u32) =
match (width.try_into(), height.try_into(), refresh.try_into()) {
(Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh),
_ => {
warn!("SetCustomMode: invalid input data");
return;
}
};
let Some(current_config) = g_state.current_state.get(output_id) else {
warn!("SetMode: output missing from the current config");
return;
};
let Some(mode) = current_config.modes.iter().find(|m| {
m.width == width
&& m.height == height
&& (refresh == 0 || m.refresh_rate == refresh)
}) else {
warn!("SetCustomMode: no matching mode");
return;
};
new_config.mode = Some(niri_ipc::ConfiguredMode {
width: mode.width,
height: mode.height,
refresh: Some(mode.refresh_rate as f64 / 1000.),
});
}
zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => {
new_config.position = Some(niri_config::Position { x, y });
}
zwlr_output_configuration_head_v1::Request::SetTransform { transform } => {
let transform = match transform {
WEnum::Value(WlTransform::Normal) => Transform::Normal,
WEnum::Value(WlTransform::_90) => Transform::_90,
WEnum::Value(WlTransform::_180) => Transform::_180,
WEnum::Value(WlTransform::_270) => Transform::_270,
WEnum::Value(WlTransform::Flipped) => Transform::Flipped,
WEnum::Value(WlTransform::Flipped90) => Transform::Flipped90,
WEnum::Value(WlTransform::Flipped180) => Transform::Flipped180,
WEnum::Value(WlTransform::Flipped270) => Transform::Flipped270,
_ => {
warn!("SetTransform: unknown requested transform");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidTransform,
"unknown transform value",
);
return;
}
};
new_config.transform = transform;
}
zwlr_output_configuration_head_v1::Request::SetScale { scale } => {
if scale <= 0. {
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidScale,
"scale is negative or zero",
);
return;
}
new_config.scale = Some(FloatOrInt(scale));
}
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
let vrr = match state {
WEnum::Value(AdaptiveSyncState::Enabled) => Some(Vrr { on_demand: false }),
WEnum::Value(AdaptiveSyncState::Disabled) => None,
_ => {
warn!("SetAdaptativeSync: unknown requested adaptative sync");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState,
"unknown adaptive sync value",
);
return;
}
};
new_config.variable_refresh_rate = vrr;
}
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrOutputHeadV1, OutputId, D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_output_head: &ZwlrOutputHeadV1,
request: zwlr_output_head_v1::Request,
_data: &OutputId,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_head_v1::Request::Release => {}
_ => unreachable!(),
}
}
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputHeadV1, data: &OutputId) {
if let Some(c) = state.output_management_state().clients.get_mut(&client) {
c.heads.remove(data);
}
}
}
impl<D> Dispatch<ZwlrOutputModeV1, (), D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_mode: &ZwlrOutputModeV1,
request: zwlr_output_mode_v1::Request,
_data: &(),
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_mode_v1::Request::Release => {}
_ => unreachable!(),
}
}
}
#[macro_export]
macro_rules! delegate_output_management{
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocols::output_management::OutputManagementManagerGlobalData
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: ()
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: u32
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: $crate::backend::OutputId
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: ()
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::protocols::output_management::OutputConfigurationHeadState
] => $crate::protocols::output_management::OutputManagementManagerState);
};
}
fn notify_removed_head(clients: &mut HashMap<ClientId, ClientData>, head: &OutputId) {
for data in clients.values_mut() {
if let Some((head, mods)) = data.heads.remove(head) {
mods.iter().for_each(|m| m.finished());
head.finished();
}
}
}
fn notify_new_head(
state: &mut OutputManagementManagerState,
output: &OutputId,
conf: &niri_ipc::Output,
) {
let display = &state.display;
let clients = &mut state.clients;
for data in clients.values_mut() {
if let Some(client) = data.manager.client() {
send_new_head::<State>(display, &client, data, *output, conf);
}
}
}
fn send_new_head<D>(
display: &DisplayHandle,
client: &Client,
client_data: &mut ClientData,
output: OutputId,
conf: &niri_ipc::Output,
) where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: 'static,
{
let new_head = client
.create_resource::<ZwlrOutputHeadV1, _, D>(display, client_data.manager.version(), output)
.unwrap();
client_data.manager.head(&new_head);
new_head.name(conf.name.clone());
// Format matches what Output::new() does internally.
new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name));
if let Some((width, height)) = conf.physical_size {
if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) {
new_head.physical_size(a, b);
}
}
let mut new_modes = Vec::with_capacity(conf.modes.len());
for (index, mode) in conf.modes.iter().enumerate() {
let new_mode = client
.create_resource::<ZwlrOutputModeV1, _, D>(display, new_head.version(), ())
.unwrap();
new_head.mode(&new_mode);
new_mode.size(i32::from(mode.width), i32::from(mode.height));
if mode.is_preferred {
new_mode.preferred();
}
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
new_mode.refresh(refresh_rate);
}
if Some(index) == conf.current_mode {
new_head.current_mode(&new_mode);
}
new_modes.push(new_mode);
}
if let Some(logical) = conf.logical {
new_head.position(logical.x, logical.y);
new_head.transform(ipc_transform_to_smithay(logical.transform).into());
new_head.scale(logical.scale);
}
new_head.enabled(conf.current_mode.is_some() as i32);
if new_head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE {
new_head.make(conf.make.clone());
}
if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE {
new_head.model(conf.model.clone());
}
if new_head.version() >= zwlr_output_head_v1::EVT_SERIAL_NUMBER_SINCE {
if let Some(serial) = &conf.serial {
new_head.serial_number(serial.clone());
}
}
if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
new_head.adaptive_sync(match conf.vrr_enabled {
true => AdaptiveSyncState::Enabled,
false => AdaptiveSyncState::Disabled,
});
}
// new_head.serial_number(output.serial);
client_data.heads.insert(output, (new_head, new_modes));
}

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