Compare commits

..

784 Commits

Author SHA1 Message Date
Ivan Molodetskikh 8ba57fcf25 Bump version to 25.05.1 2025-05-25 08:45:41 +03:00
Ivan Molodetskikh 126ca37d96 Rename Un/Set/ToggleUrgent to Un/Set/ToggleWindowUrgent
Overlooked this when reviewing. This change is not cfg-breaking (since you
can't bind these directly), but it does break calling these actions through
IPC. I don't imagine they are widely used though, and the original PR author
who also implemented urgency for bars said he didn't use these actions either.
2025-05-25 08:43:27 +03:00
Ivan Molodetskikh e6bd60fbb1 wiki: Remove note about numlock issue
It's been fixed.
2025-05-25 08:36:33 +03:00
Ivan Molodetskikh a605a3f016 Account for hidden pointer in move_cursor() 2025-05-23 23:08:51 +03:00
Ivan Molodetskikh ef44adea69 Set pointer contents straight to nothing when disabling pointer 2025-05-23 23:08:51 +03:00
Duncan Overbruck 7fdb918cd0 input: do not revert fully invisible cursor to hidden (#1650)
* input: do not force redraw to hide an already hidden cursor

* more

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-05-23 05:24:24 +00:00
Ivan Molodetskikh 8347cc20dc Update Smithay (pen tilt, num lock, keymap spam) 2025-05-22 18:05:17 +03:00
alex-huff 51a176ec4a layer-shell: only reset 'initial_configure_sent' for mapped surfaces 2025-05-22 08:02:56 -07:00
alex-huff d618daf6b9 layer-shell: don't dismiss popups because of unmapped layer surfaces
Fixes #1640
2025-05-22 07:55:29 -07:00
Ivan Molodetskikh 357f9157cc wiki/packaging: Mention RUN_SLOW_TESTS 2025-05-22 11:37:09 +03:00
Ivan Molodetskikh c4a759e620 wiki/packaging: Document limiting test threads 2025-05-22 11:35:12 +03:00
Ivan Molodetskikh f369a0f810 input: Add missing check for output under 2025-05-22 08:55:01 +03:00
Ivan Molodetskikh 71251a7003 input: Add missing redraws on urgency actions
The layout urgent colors update even without window rule changes.
2025-05-21 19:50:13 +03:00
alex-huff 2415346caa layer-shell: properly handle re-map
According to the zwlr_layer_surface_v1 documentation: Unmapping a
layer_surface means that the surface cannot be shown by the compositor
until it is explicitly mapped again. The layer_surface returns to the
state it had right after layer_shell.get_layer_surface. The client can
re-map the surface by performing a commit without any buffer attached,
waiting for a configure event and handling it as usual.

Before this commit, no configure event was sent when a client performed
a commit without any buffer attached.
2025-05-21 07:25:22 -07:00
Ivan Molodetskikh 3f2b7e63ba Improve comment in on-demand layer-shell keyboard alive check 2025-05-19 09:18:07 +03:00
Ivan Molodetskikh ae89cb6017 Update README.md 2025-05-17 15:59:05 +03:00
Ivan Molodetskikh b6fc4d0455 wiki/Overview: Link the new video 2025-05-17 14:38:24 +03:00
Federico Ceratto d8265ad34e Stop including broken LFS files in source tarball 2025-05-17 04:29:46 -07:00
Ivan Molodetskikh 3b864dc104 Bump version to 25.05 2025-05-17 13:50:36 +03:00
Ivan Molodetskikh 15093221ed wiki/Overview: Update wording 2025-05-17 13:44:55 +03:00
Ivan Molodetskikh ac7b3fbf19 wiki: Link to issue in numlock
https://github.com/YaLTeR/niri/issues/1501
2025-05-17 07:58:52 +03:00
Ivan Molodetskikh bb8eb377c7 Update dependencies more carefully
No winit deadlock in this update.
2025-05-16 22:54:37 +03:00
Ivan Molodetskikh 6169c0312a Revert "Update dependencies"
Something is causing winit deadlock on nested niri exit.

This reverts commit 2ae99224ab.

This reverts commit 0d6843ea67.
2025-05-16 22:53:54 +03:00
Ivan Molodetskikh 2ae99224ab Update dependencies 2025-05-16 22:29:09 +03:00
Ivan Molodetskikh 4f63e13385 Deal with new Clippy warnings 2025-05-16 22:21:14 +03:00
Ivan Molodetskikh 46a8f81160 ipc/client: Make compositor version check for JSON parsing errors
These can happen when adding new fields to returned structs.
2025-05-15 09:08:53 +03:00
Ivan Molodetskikh 0d6843ea67 Update dependencies 2025-05-13 17:13:35 +03:00
Ivan Molodetskikh 6d083ea497 layout: Fix workspace swipe to same workspace forgetting previous id
This manifested much more prominently in the overview.
2025-05-13 08:17:15 +03:00
Ivan Molodetskikh 7a42140d6c dependabot: Change to weekly
Let's see if this fixes it missing from the GitHub UI.
2025-05-12 20:05:33 +03:00
Ivan Molodetskikh eeb411bef5 wiki: Add Since for touchpad drag 2025-05-12 20:05:10 +03:00
Ivan Molodetskikh defd4c5c4d Add center-visible-columns action 2025-05-12 14:13:51 +03:00
dependabot[bot] 7227e64149 build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.37 to 4.5.38
- [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.37...clap_complete-v4.5.38)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 02:58:56 -07:00
Ivan Molodetskikh c98537a2b0 Implement baba-is-float for layers 2025-05-12 09:10:59 +03:00
Ivan Molodetskikh 9c103f1f1d Add missing "to" in comment 2025-05-12 08:26:39 +03:00
Ivan Molodetskikh 2aff1ec71a ipc/socket: Support multiple requests 2025-05-11 21:51:26 -07:00
Jon Heinritz 3466fc0a66 ipc: document the new socket behavior 2025-05-11 21:51:26 -07:00
Jon Heinritz f917932b3e ipc: support long living sockets 2025-05-11 21:51:26 -07:00
Ivan Molodetskikh 89b7423ee5 Print urgent status in niri msg windows 2025-05-10 23:43:00 +03:00
Ivan Molodetskikh a2efaf2816 Add is-urgent window rule matcher 2025-05-10 22:49:55 +03:00
Ivan Molodetskikh 5816691460 Add urgent color support to tab indicators 2025-05-10 22:42:45 +03:00
Ivan Molodetskikh 4b5e9e6cb0 wiki: Document urgent-color 2025-05-10 22:42:45 +03:00
Duncan Overbruck a8259b4cea add WindowUrgencyChanged ipc event 2025-05-10 12:14:41 -07:00
Duncan Overbruck 9d3d7cb0e9 add {toggle,set,unset}-urgent cli actions 2025-05-10 12:14:41 -07:00
Duncan Overbruck 398bc78ea0 add urgent border color and gradient 2025-05-10 12:14:41 -07:00
Duncan Overbruck caa6189448 add workspace urgency ipc event 2025-05-10 12:14:41 -07:00
Duncan Overbruck 86f57c2ec7 add window urgency through xdg-activation-v1
urgency is done through activation requests without a serial from a
previous interaction.

https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
2025-05-10 12:14:41 -07:00
Charlie Le 3cc67897af Implement IPC for the overview state (#1526)
* Implement IPC for the overview state

* Update Overview IPC to maintain naming consistency, renamed OverviewToggled to be more clear, simplify overview state request on the server, consolidate ipc refresh

* Fix Overview is_open in IPC client

* Change opened to is_open

* Update niri-ipc/src/lib.rs

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

* Update niri-ipc/src/state.rs

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

* Update src/ipc/client.rs

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

* Update src/ipc/client.rs

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

* Add overview state to EventStreamStatePart replicate and apply

* Fix formatting

* Rename Overview to OverviewState

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-05-09 18:01:01 +03:00
dependabot[bot] a99489c6c0 build(deps): bump clap_complete in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap_complete](https://github.com/clap-rs/clap).


Updates `clap_complete` from 4.5.49 to 4.5.50
- [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.49...clap_complete-v4.5.50)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 07:38:04 -07:00
Ivan Molodetskikh 0763c7e196 Add a clickable button to capture the screenshot
Allows tablet-, touch- and mouse-only confirmation.
2025-05-09 15:42:23 +03:00
Ivan Molodetskikh fb5c5204e8 Extract confirm_screenshot() 2025-05-09 15:41:57 +03:00
Ivan Molodetskikh d207cd385b screenshot_ui: Refactor mouse down + touch slot state 2025-05-09 15:10:00 +03:00
Ivan Molodetskikh 99bf2df2b4 Silence new zvariant De/SerializeDict deprecations
Questionable exercise converting to serde with much more boilerplate, and
breaking compat with older zvariant versions. Plus maybe this will be
undeprecated back.
2025-05-09 10:35:16 +03:00
Ivan Molodetskikh 09be90f4e6 Add touch selection support to the screenshot UI 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh dfc42b9d82 Split ScreenshotUi::pointer_down() and up() 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh e2b9838d89 Extract evt.slot() 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 816a0d479c Rename touch_location to pos 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 84323d10a4 Support tablet input for screenshot UI selection 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh b956f2775c Use early return 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 9ff2f83db0 Simplify ScreenshotUi::pointer_button() 2025-05-09 10:28:20 +03:00
James Sully 7a10f71ee5 refactor(main): eliminate a mut from config load code in main
I think this makes for marginally better readability, since you don't
have to wonder whether config_errored is set anywhere else. It's also
slightly terser.
2025-05-09 00:25:54 -07:00
James Sully ea7add3563 fix: don't try to create a default config at path that exists
Currently this bug has no actual consequences, we just continue silently
on AlreadyExists in main()
(this line: https://github.com/YaLTeR/niri/blob/e9c6f08906143c3fec1ad1301d538bef4cbc1978/src/main.rs#L151).

This commit just eliminates the redundant attempt.
2025-05-08 21:52:39 -07:00
Ivan Molodetskikh e9c6f08906 Add a resize transaction client-server test 2025-05-07 22:59:57 +03:00
Ivan Molodetskikh 17343a6740 wiki: Fix Until note location 2025-05-06 17:42:14 +03:00
Ivan Molodetskikh 140d726cd3 wiki: Clarify that layers within backdrop ignore input 2025-05-06 17:40:52 +03:00
Ivan Molodetskikh c37d3b3442 wiki: Link to output backdrop-color from overview {} 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 497f186422 Add layout background-color setting 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 3e31c134a6 Implement place-within-backdrop layer rule 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh fe682938db Simplify exclusive focus on layer check 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 6142922ca4 wiki: Mention Overview behavior on layer-shell page 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 4b44fba14c wiki: Clarify FAQ question about border with background 2025-05-06 17:34:40 +03:00
dependabot[bot] 57639ca84c build(deps): bump the rust-dependencies group across 1 directory with 3 updates
Bumps the rust-dependencies group with 3 updates in the / directory: [clap_complete](https://github.com/clap-rs/clap), [glam](https://github.com/bitshifter/glam-rs) and [zbus](https://github.com/dbus2/zbus).


Updates `clap_complete` from 4.5.48 to 4.5.49
- [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.48...clap_complete-v4.5.49)

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

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

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.5.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zbus
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-06 01:38:03 -07:00
Ivan Molodetskikh ec88aae77d wiki: Add Since to numlock 2025-05-05 20:29:55 +03:00
Ivan Molodetskikh 6c9705dd4b layout/scrolling: Update view offset on config update
Fix always-centering not applied right away. No other changes intended.
2025-05-01 21:37:34 +03:00
Aberter Yan eb590c5346 Implement --focus for MoveColumnToWorkspace/Up/Down 2025-05-01 11:06:34 -07:00
Ivan Molodetskikh 02baad91ac wiki: Clarify how key bindings are resolved 2025-05-01 16:28:28 +03:00
Ivan Molodetskikh 68589cd5a1 wiki: Remove "experimental" from custom shaders
They've been around for a while.
2025-05-01 11:19:38 +03:00
Ivan Molodetskikh f2c690802b Adjust the workspace shadow defaults some more 2025-05-01 11:04:47 +03:00
Ivan Molodetskikh 9d6037b94c Normalize workspace shadows to 1080 px tall screen, adjust defaults
Workspace gaps are dependent on screen size, so it makes sense to make shadows
depend on the screen size to, to avoid them filling more or less of the gap.
2025-05-01 10:33:53 +03:00
Ivan Molodetskikh 7b4cf094ef Draw workspace shadows behind all workspaces 2025-05-01 10:10:11 +03:00
Ivan Molodetskikh 446bc155ce Add workspace-shadow {} config to overview {} 2025-05-01 09:45:38 +03:00
Ivan Molodetskikh 3289324ce4 wiki: Use subheadings for overview settings 2025-05-01 09:36:39 +03:00
Ivan Molodetskikh 9fb02b9571 layout: Fix DnD scroll not stopping when interactive moving unfullscreen to floating 2025-04-30 20:32:56 +03:00
erdii 0e9496b01e chore(wiki): document numlock setting
Co-Authored-By: Ivan Molodetskikh <yalterz@gmail.com>
Signed-off-by: erdii <me@erdii.engineering>
2025-04-30 09:54:19 -07:00
erdii 82dabc21f3 feat: implement support to enable numlock at startup
Signed-off-by: erdii <me@erdii.engineering>
2025-04-30 09:54:19 -07:00
erdii 39b3d62873 chore: bump smithay 2025-04-30 09:54:19 -07:00
dependabot[bot] af080a03cd build(deps): bump the rust-dependencies group with 3 updates
Bumps the rust-dependencies group with 3 updates: [wayland-backend](https://github.com/smithay/wayland-rs), [insta](https://github.com/mitsuhiko/insta) and [wayland-client](https://github.com/smithay/wayland-rs).


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

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

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

---
updated-dependencies:
- dependency-name: wayland-backend
  dependency-version: 0.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.43.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wayland-client
  dependency-version: 0.31.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-30 01:46:05 -07:00
Ivan Molodetskikh 5f117c61dc animation/spring: Guard against numerical instability 2025-04-29 10:51:53 +03:00
Christian Meissl cb857e32e4 Bump Smithay and others
Presentation subsurface fix, popup unconstrain resize fix, cursor shape fix, refactors.
2025-04-29 08:53:25 +03:00
dependabot[bot] 199be26947 build(deps): bump the rust-dependencies group across 1 directory with 5 updates
Bumps the rust-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.97` | `1.0.98` |
| [clap](https://github.com/clap-rs/clap) | `4.5.34` | `4.5.37` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.1` | `0.30.2` |
| [libc](https://github.com/rust-lang/libc) | `0.2.171` | `0.2.172` |
| [insta](https://github.com/mitsuhiko/insta) | `1.42.2` | `1.43.0` |



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

Updates `clap` from 4.5.34 to 4.5.37
- [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.34...clap_complete-v4.5.37)

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

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

Updates `insta` from 1.42.2 to 1.43.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.42.2...1.43.0)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.98
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.37
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.172
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 22:35:11 -07:00
Ivan Molodetskikh d5c0c74d2c Fix hot corners preventing focus even when disabled 2025-04-29 08:24:45 +03:00
Ivan Molodetskikh 9bb292ec82 default-config: Set repeat=off for the Overview bind 2025-04-28 12:05:55 +03:00
Ivan Molodetskikh a1ba6bcaa0 wiki: Update backdrop-color examples 2025-04-28 09:32:44 +03:00
Ivan Molodetskikh fd389af6d8 Add backdrop-color setting to overview {} 2025-04-28 09:14:43 +03:00
Ivan Molodetskikh db09727b18 Replace Smithay's SolidColor elements with ours
Must've forgotten about these back when I replaced others.
2025-04-28 09:05:55 +03:00
Ivan Molodetskikh c9d6478c3c wiki: Rename Configuration: Overview page to Introduction 2025-04-28 07:54:02 +03:00
bogdanov 758cca5432 Fix pointer hiding so that it is no longer annoying (#1426)
* replace `pointer_hidden` with `pointer_visiblity`

* disable hidden pointer after content underneath has changed

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-04-27 06:25:36 +00:00
Ivan Molodetskikh 78e3daf5f8 overview: Activate window upon dropping from interactive move 2025-04-26 13:29:36 +03:00
Mitchel Stewart a99a0b2492 Steam Black Screen system-composer 2025-04-26 00:38:48 -07:00
Ivan Molodetskikh bfd42c74f4 layout/tab_indicator: Fix negative gap
Regressed in a recent commit that added max1.
2025-04-26 09:25:31 +03:00
Ivan Molodetskikh 501ea47128 wiki/Overview: Mention backdrop-color 2025-04-25 17:56:17 +03:00
Ivan Molodetskikh d2a1cf53b4 Fix panic when interactively moving to invisible workspace
Introduced in the interactive move between workspaces commit.
2025-04-25 16:55:36 +03:00
Ivan Molodetskikh 62d47d77d5 wiki: Document backdrop-color and overview-open-close animation 2025-04-25 15:29:42 +03:00
Ivan Molodetskikh 85cd64e830 Document the Overview and other new things 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 55c14eebf2 hotkey_overlay: Show the ToggleOverview bind 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 3fe67549b4 default-config: Bind Mod+O to toggle-overview 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 1835b532d9 Implement interactive move to a new workspace above/between 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh e6d82d3ee3 Implement top-left hot corner to toggle the Overview
Compared to third-party implementations such as waycorner:

- It works during interactive window move (no surfaces receive pointer
  focus in this case, so this cannot work through layer-shell).
- It works during drag-and-drop.
- It disables itself over fullscreen windows.
- It does not prevent direct scanout.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh fae3a27641 Implement DnD hold to activate window or workspace 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 31e76cf451 overview: Add DnD up/down scrolling 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh b8a9be542f overview: Add touchscreen gestures 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 59de6918b3 overview: Add two-finger touchpad scroll 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh bd3d554389 overview: Add hardcoded mouse scroll binds 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh af1fca35bb Implement an Overview 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 9571d149b2 Render workspaces separately with gaps between
This design makes more sense spatially, and is required for the
Overview. Gaps also make it clear how clipping windows to workspace
bounds works.

Background and bottom layer-shell surfaces get duplicated for each
workspace, while top and overlay stay "on top".
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 99358e36b3 layout/monitor: Extract activate_workspace_with_anim_config() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 8b878f355f Put interactively moved window on top of background and bottom layer popups 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 395b6d9a4f layout: Extract interactive_moved_window_under() and add output check
Fixes interactively moved window getting input on every output rather
than just its own.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 25f24f668c Extract mapped_hit_data() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 929eaf0d69 Pass target workspace to view offset grab 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ce3103949f layout/scrolling: Support view offset anim during gesture
Brings back moving the newly active window into focus upon interactive
move dragging out.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ef60dd81d7 layout/monitor: Cache scale, view_size, working_area 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 7671a5d833 layout/monitor: Don't consider workspace switch in active_tile_visual_rectangle()
This only did something when in the middle of a touchpad gesture, and it
didn't really make sense for that edge case.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 3f09352067 layout/monitor: Extract add_workspace_at() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 5059cce886 Add with_alpha() to shader and shadow element 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh b20dd226c0 layout: Move insert hint from ScrollingSpace to Monitor 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh acb69c3b4d layout: Return floating and scrolling elems separately from Workspace 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh dbe0a9e293 layout/tab_indicator: Use round_max1 where appropriate 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh d3a79faeec layout/monitor: Extract workspace_render_idx() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 21630ddb5e layout/monitor: Extract workspaces_render_geo() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 9e5e0c85bb Simplify condition 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 5cd8040d1a Extract is_layout_obscured_under() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 86351938f2 Put the top layer above bottom and background layer popups
Makes it consistent with how window popups are below the top layer, also
will make more sense for the overview.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ee4c5e23ab Reformat scroll factor computation 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ffd6acc0aa layout/monitor: Extract WorkspaceSwitchGesture::min_max() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh cee11dc329 layout/monitor: Keep track of workspace switch gesture start idx
Fixes jump when "catching" an animation with a gesture.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 59a42249a4 Remove cancellation from swipe gestures
It only worked for workspace switch, and even there it was more confusing than
helpful.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 74b016202b Add missing bounds checks to move-workspace actions
Fixes panics.
2025-04-25 10:54:09 +03:00
Ivan Molodetskikh 6ab055a4b9 niri.spec.rpkg: Recommend waybar
Now it's spawned by the default config.
2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 98bd9b7abb niri.spec.rpkg: Fix License 2025-04-22 22:51:56 +03:00
Ivan Molodetskikh f36e1c2ef2 default-config: Spawn waybar at startup
Make it a bit less of an empty screen.
2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 2243615fe9 default-config: Set titles for the default-bound apps 2025-04-22 22:51:56 +03:00
Ivan Molodetskikh 7884d3bfea layout: Extract Monitor::update_shaders() 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh fdbc485d78 layout: Remove width and is_full_width from InsertHint
They were unused.
2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 7e253d2687 layout: Don't pass scale to render unnecessarily
These parts of the layout already know their scale.
2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 15ba2ab300 Rename render_floating_for_output to render_interactive_move_for_output 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 37840a418a animation: Extract value_at() and fix animations off difference 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh 4a4c972ffb animation: Add more getters 2025-04-17 11:31:34 +03:00
Ivan Molodetskikh ba933773ab animation: Fix restarted() Spring using old from/to 2025-04-16 07:46:10 +03:00
Ivan Molodetskikh f1cca1a6ca Back out "chore: update smithay"
This backs out commit 763cd564e3.

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

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

* Update niri-ipc/src/lib.rs

---------

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

* Fix incorrect wiki string

* Center mouse on start

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

* fixes

---------

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


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

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

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

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

* input: run fmt

* Reimplement screenshot UI binds in a better way

---------

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

* lib: update comment

* misc: minor dup refactor

* input: format code

* layout: minor nit

* layout: update comment

* input: remove unnecessary conditionals

* misc: replace boolean

* tests: fix the failing one

* layout: change to smart

* ipc: Option<bool> -> bool

* lib: format code

* Rewrite focus doc comment

---------

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

* Update wiki/Important-Software.md

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

---------

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

* Update wiki/Application-Issues.md

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

* wiki: remove Arch specific files for Electron

* Apply suggestions from code review

---------

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

* Update wiki/Application-Issues.md

---------

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

refactor: improve quality

feat: implement gnomes PickColor method

refactor: minor code extraction

misc: fix reviews

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


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

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

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

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

* don't depend on build right now

* fix fragment

* reintroduce dependency for build

* don't only check links on push to main

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

* change commented suggestions, try v2.0.2 for action

* describe why we're on v2.0.2

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

* wiki updates

* use .md with anchors, revert sidebar

* bump wiki action

* add some more anchors, fix some language

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


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

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

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

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


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

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

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


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

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

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

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


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

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

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

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


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

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

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

* Update src/cli.rs

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

---------

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

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


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

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

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

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

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

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

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

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

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

* Update wiki/Configuration:-Input.md

---------

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

* feat: implemented extra alignement

* feat: added example

* doc: updated documentation with extra alignements

* doc: moved example in wiki and typo correction

* fix: relative position should be positive and not negative

* fixes

---------

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


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

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

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

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

* Update wiki/Application-Issues.md

---------

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

* fixes

---------

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


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

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

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

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

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


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

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

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

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

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


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

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

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

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


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

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

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

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

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

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


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

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

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

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

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


Updates `pango` from 0.20.7 to 0.20.9
- [Release notes](https://github.com/gtk-rs/gtk-rs-core/releases)
- [Changelog](https://github.com/gtk-rs/gtk-rs-core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gtk-rs/gtk-rs-core/compare/0.20.7...0.20.9)

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

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

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


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

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

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


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

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

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

* Update wiki/Application-Issues.md

---------

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

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


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

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

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


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

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

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

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

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

* Update wiki/Configuration:-Miscellaneous.md

---------

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

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

* Apply suggestions from code review

---------

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

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

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

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

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


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

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

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

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

* Update src/input/mod.rs

---------

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

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

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

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

* Added back `self.workspace_switch = None`

* Reordered some workspace cleanup logic

* Fix formatting

* Add missing blank lines

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

* Move function up and add fixme comment

---------

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


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

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

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


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

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

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

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


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

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

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

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

* implement keyboard-shortcuts-inhibit

* implement virtual-pointer

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

* add toggle-keyboard-shortcuts-inhibit bind

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

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

* Add some tests for is_inhibiting_shortcuts

---------

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


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

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

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


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

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

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

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


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

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

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

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


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

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

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

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

* Update resources/niri-session

---------

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

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

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


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

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

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

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

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

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

* Update wiki/Getting-Started.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-01-13 06:59:21 +03:00
sodiboo 6e41220dbf use standard padding syntax instead of implementing our own
the padding of the two-digit-month can be implemented much more
concisely using `std::fmt` syntax.
2025-01-12 21:38:51 +03:00
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
5235 changed files with 103864 additions and 12270 deletions
+11
View File
@@ -1 +1,12 @@
# LFS configuration for images from the wiki
*.png filter=lfs diff=lfs merge=lfs -text
# Exclude LFS-tracked files from the tarball
/wiki/img/ export-ignore
# exclude .gitattributes itself from the tarball
.gitattributes export-ignore
# tip: can be tested using
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
+15 -1
View File
@@ -9,9 +9,23 @@ assignees: ''
<!-- Please describe the issue here at the top, then fill in the system information below. -->
<!-- Attaching your full niri config can help diagnose the problem. -->
<!--
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
You can also check what process the window PID belongs to:
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
If this points to xwayland-satellite, then it's an X11 window.
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
-->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
* niri version:
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
+6
View File
@@ -2,3 +2,9 @@ contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
- name: Ask a question
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
about: Chat about niri with other users
+22
View File
@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
groups:
smithay:
patterns:
- "smithay"
- "smithay-drm-extras"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
+76 -31
View File
@@ -9,6 +9,8 @@ on:
env:
RUN_SLOW_TESTS: 1
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
jobs:
build:
@@ -23,8 +25,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 +34,8 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
@@ -63,19 +64,26 @@ 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
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
visual-tests:
# Job that runs randomized tests for a longer period of time.
randomized-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
name: randomized tests
runs-on: ubuntu-24.04
env:
RUST_BACKTRACE: 1
PROPTEST_CASES: 200000
PROPTEST_MAX_LOCAL_REJECTS: 200000
PROPTEST_MAX_GLOBAL_REJECTS: 200000
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v4
@@ -84,8 +92,35 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests --release
- name: Test
run: cargo test --all --exclude niri-visual-tests --release
visual-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
@@ -98,9 +133,8 @@ jobs:
strategy:
fail-fast: false
name: 'msrv - 1.77.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
name: msrv
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -109,10 +143,10 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@1.77.0
- uses: dtolnay/rust-toolchain@1.80.1
- uses: Swatinem/rust-cache@v2
@@ -123,8 +157,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 +166,8 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -146,7 +179,7 @@ jobs:
run: cargo clippy --all --all-targets
rustfmt:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -161,8 +194,8 @@ jobs:
run: cargo fmt --all -- --check
fedora:
runs-on: ubuntu-22.04
container: fedora:39
runs-on: ubuntu-24.04
container: fedora:41
steps:
- uses: actions/checkout@v4
@@ -172,13 +205,13 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel libdisplay-info-devel
sudo dnf install -y ${{ env.DEPS_DNF }} 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:
@@ -192,27 +225,39 @@ jobs:
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- run: nix build
- run: nix flake check
continue-on-error: true
check-links:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
with:
args: --offline --include-fragments 'wiki/*.md'
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
needs:
- build
- check-links
permissions:
contents: write
runs-on: ubuntu-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
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
rustdoc:
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
+63
View File
@@ -0,0 +1,63 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
version:
description: 'Public version'
required: true
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
RUN_SLOW_TESTS: 1
jobs:
prepare-release:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
- name: Create vendored dependencies archive
run: |
mkdir .cargo
cargo vendor --locked > .cargo/config.toml
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
- name: Build
run: cargo build --all --frozen --release
- name: Build tests
run: cargo test --no-run --all --frozen --release
- name: Test
run: cargo test --all --frozen --release -- --nocapture
- name: Draft release
uses: softprops/action-gh-release@v2
with:
draft: true
tag_name: v${{ github.event.inputs.version }}
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
fail_on_unmatched_files: true
Generated
+1189 -1174
View File
File diff suppressed because it is too large Load Diff
+59 -44
View File
@@ -1,32 +1,38 @@
[workspace]
members = ["niri-visual-tests"]
members = [
"niri-config",
"niri-ipc",
"niri-visual-tests",
]
[workspace.package]
version = "0.1.9"
version = "25.5.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.77"
rust-version = "1.80.1"
[workspace.dependencies]
anyhow = "1.0.88"
bitflags = "2.6.0"
clap = { version = "4.5.17", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
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.3", default-features = false }
anyhow = "1.0.98"
bitflags = "2.9.1"
clap = { version = "4.5.38", features = ["derive"] }
insta = "1.43.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracy-client = { version = "0.18.0", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
@@ -47,45 +53,45 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "1.13.0", optional = true }
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.18.0", features = ["derive"] }
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
bytemuck = { version = "1.23.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.8.0"
fastrand = "2.1.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
clap_complete = "4.5.50"
directories = "6.0.0"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.29.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
glam = "0.30.3"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.158"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.9", path = "niri-config" }
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.2.2"
pango = { version = "0.20.1", features = ["v1_44"] }
pangocairo = "0.20.1"
libc = "0.2.172"
libdisplay-info = "0.2.2"
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.5.1", path = "niri-config" }
niri-ipc = { version = "25.5.1", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.0.0"
pango = { version = "0.20.10", features = ["v1_44"] }
pangocairo = "0.20.10"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.13"
portable-atomic = { version = "1.7.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.2"
png = "0.17.16"
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.5"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.2", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.10"
wayland-scanner = "0.31.6"
xcursor = "0.3.8"
zbus = { version = "~3.15.2", optional = true }
zbus = { version = "5.7.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -107,15 +113,18 @@ features = [
[dev-dependencies]
approx = "0.5.1"
k9.workspace = true
proptest = "1.5.0"
proptest-derive = "0.5.0"
xshell = "0.2.6"
calloop-wayland-source = "0.4.0"
insta.workspace = true
proptest = "1.6.0"
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
rayon = "1.10.0"
wayland-client = "0.31.10"
xshell = "0.2.7"
[features]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-io", "notify-rust", "url"]
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
@@ -124,6 +133,8 @@ xdp-gnome-screencast = ["dbus", "pipewire"]
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 = []
@@ -136,8 +147,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.9"
version = "25.02"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+47 -9
View File
@@ -1,7 +1,7 @@
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
@@ -10,7 +10,7 @@
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
## About
@@ -28,12 +28,15 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
@@ -45,10 +48,35 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Here are some points you may have questions about:
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
- **NVIDIA**: seems to work fine.
- **Floating windows**: yes, starting from niri 25.01.
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
We have touchpad gestures, but no touchscreen gestures yet.
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
- Steam and games, including Proton: work perfectly through xwayland-satellite.
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
- Discord and other Electron apps: work well through xwayland-satellite.
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
@@ -62,10 +90,17 @@ Here are some other projects which implement a similar workflow:
- [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.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Media
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
@@ -75,6 +110,9 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[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
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[gamescope]: https://github.com/ValveSoftware/gamescope
Generated
+21 -95
View File
@@ -1,72 +1,12 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1724533099,
"narHash": "sha256-ZIDtvVQHoCkNoBlLUB3wmqbqCb0Es3DfdUGeDI/58aY=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7543c8d76f91b8844e0f3b3cc347a72d8fb4b49e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1722493751,
"narHash": "sha256-l7/yMehbrL5d4AI8E2hKtNlT50BlUAau4EKTgPg9KcY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "60ab4a085ef6ee40f2ef7921ca4061084dd8cf26",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
@@ -77,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1724395761,
"narHash": "sha256-zRkDV/nbrnp3Y8oCADf5ETl1sDrdmAW6/bBVJ8EbIdQ=",
"lastModified": 1742707865,
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ae815cee91b417be55d43781eb4b73ae1ecc396c",
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
"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": 1722449213,
"narHash": "sha256-1na4m2PNH99syz2g/WQ+Hr3RfY7k4H8NBnmkr5dFDXw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c8e41d95061543715b30880932ec3dc24c42d7ae",
"lastModified": 1742697269,
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"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"
}
}
+234 -77
View File
@@ -4,101 +4,258 @@
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.mkLib pkgs).overrideToolchain toolchain;
outputs =
{
self,
nixpkgs,
nix-filter,
rust-overlay,
}:
let
niri-package =
{
lib,
cairo,
dbus,
libGL,
libdisplay-info,
libinput,
seatd,
libxkbcommon,
libgbm,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
installShellFiles,
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
installShellFiles
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libdisplay-info
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
cairo
glib
pixman
];
buildInputs =
[
cairo
dbus
libGL
libdisplay-info
libinput
seatd
libxkbcommon
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
xorg.libXcursor
xorg.libXi
libxkbcommon
];
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";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath craneArgs.runtimeDependencies; # Needed for tests to find libxkbcommon
# 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 =
''
installShellCompletion --cmd niri \
--bash <($out/bin/niri completions bash) \
--fish <($out/bin/niri completions fish) \
--zsh <($out/bin/niri completions zsh)
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
''
+ 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 = craneLib.devShell {
inputsFrom = [niri];
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"
];
}
))
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (craneArgs.runtimeDependencies ++ craneArgs.nativeBuildInputs ++ craneArgs.buildInputs);
inherit (niri) LIBCLANG_PATH;
};
}
);
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 { };
};
};
}
+5 -6
View File
@@ -11,14 +11,13 @@ repository.workspace = true
bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.9", path = "../niri-ipc" }
regex = "1.10.6"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.5.1", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.0"
insta.workspace = true
pretty_assertions = "1.4.1"
+30
View File
@@ -0,0 +1,30 @@
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
#[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>,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
+2349 -308
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)
}
}
+6 -2
View File
@@ -1,15 +1,19 @@
[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 }
schemars = { version = "0.8.22", optional = true }
serde.workspace = true
serde_json.workspace = true
+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.5.1"
```
+595 -15
View File
@@ -1,8 +1,23 @@
//! Types for communicating with niri via IPC.
//!
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
//! can keep reading [`Event`]s from the socket after the response.
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
//! wrapping a [`Response`].
//!
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
//! event stream and write more requests at the same time, you need to use two IPC sockets.
//!
//! <div class="warning">
//!
//! Requests are *always* processed separately. Time passes between requests, even when sending
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
//! new workspace in-between the two responses). This goes for actions too: sending
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
//! the wrong window because a different window got focused in-between these requests.
//!
//! </div>
//!
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
@@ -12,13 +27,29 @@
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
//! separate line.
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! on a single line each.
//!
//! ## 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.5.1"
//! ```
//!
//! ## Features
//!
//! This crate defines the following features:
//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
//! the types.
//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
#![warn(missing_docs)]
use std::collections::HashMap;
@@ -41,12 +72,18 @@ pub enum Request {
Workspaces,
/// Request information about open windows.
Windows,
/// Request information about layer-shell surfaces.
Layers,
/// Request information about the configured keyboard layouts.
KeyboardLayouts,
/// Request information about the focused output.
FocusedOutput,
/// Request information about the focused window.
FocusedWindow,
/// Request picking a window and get its information.
PickWindow,
/// Request picking a color from the screen.
PickColor,
/// Perform an action.
Action(Action),
/// Change output configuration temporarily.
@@ -77,6 +114,8 @@ pub enum Request {
EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
/// Request information about the overview.
OverviewState,
}
/// Reply from niri to client.
@@ -105,14 +144,38 @@ pub enum Response {
Workspaces(Vec<Workspace>),
/// Information about open windows.
Windows(Vec<Window>),
/// Information about layer-shell surfaces.
Layers(Vec<LayerSurface>),
/// Information about the keyboard layout.
KeyboardLayouts(KeyboardLayouts),
/// Information about the focused output.
FocusedOutput(Option<Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
/// Information about the picked window.
PickedWindow(Option<Window>),
/// Information about the picked color.
PickedColor(Option<PickedColor>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about the overview.
OverviewState(Overview),
}
/// Overview information.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Overview {
/// Whether the overview is currently open.
pub is_open: bool,
}
/// Color picked from the screen.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PickedColor {
/// Color values as red, green, blue, each ranging from 0.0 to 1.0.
pub rgb: [f64; 3],
}
/// Actions that niri can perform.
@@ -132,6 +195,8 @@ pub enum Action {
},
/// Power off all monitors via DPMS.
PowerOffMonitors {},
/// Power on all monitors via DPMS.
PowerOnMonitors {},
/// Spawn a command.
Spawn {
/// Command to spawn.
@@ -145,9 +210,23 @@ pub enum Action {
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot {},
Screenshot {
/// Whether to show the mouse pointer by default in the screenshot UI.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
/// Screenshot the focused screen.
ScreenshotScreen {},
ScreenshotScreen {
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
/// Whether to include the mouse pointer in the screenshot.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
ScreenshotWindow {
@@ -156,7 +235,14 @@ pub enum Action {
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// Write the screenshot to disk in addition to putting it in your clipboard.
///
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
},
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
ToggleKeyboardShortcutsInhibit {},
/// Close a window.
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
CloseWindow {
@@ -178,12 +264,34 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle windowed (fake) fullscreen on a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
)]
ToggleWindowedFullscreen {
/// Id of the window to toggle windowed fullscreen of.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus a window by id.
FocusWindow {
/// Id of the window to focus.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus a window in the focused column by index.
FocusWindowInColumn {
/// Index of the window in the column.
///
/// The index starts from 1 for the topmost window.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Focus the previously focused window.
FocusWindowPrevious {},
/// Focus the column to the left.
FocusColumnLeft {},
/// Focus the column to the right.
@@ -196,6 +304,14 @@ pub enum Action {
FocusColumnRightOrFirst {},
/// Focus the next column to the left, looping if at start.
FocusColumnLeftOrLast {},
/// Focus a column by index.
FocusColumn {
/// Index of the column to focus.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Focus the window or the monitor above.
FocusWindowOrMonitorUp {},
/// Focus the window or the monitor below.
@@ -220,6 +336,14 @@ pub enum Action {
FocusWindowOrWorkspaceDown {},
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp {},
/// Focus the topmost window.
FocusWindowTop {},
/// Focus the bottommost window.
FocusWindowBottom {},
/// Focus the window below or the topmost window.
FocusWindowDownOrTop {},
/// Focus the window above or the bottommost window.
FocusWindowUpOrBottom {},
/// Move the focused column to the left.
MoveColumnLeft {},
/// Move the focused column to the right.
@@ -232,6 +356,14 @@ pub enum Action {
MoveColumnLeftOrToMonitorLeft {},
/// Move the focused column to the right or to the monitor to the right.
MoveColumnRightOrToMonitorRight {},
/// Move the focused column to a specific index on its workspace.
MoveColumnToIndex {
/// New index for the column.
///
/// The index starts from 1 for the first column.
#[cfg_attr(feature = "clap", arg())]
index: usize,
},
/// Move the focused window down in a column.
MoveWindowDown {},
/// Move the focused window up in a column.
@@ -240,16 +372,62 @@ pub enum Action {
MoveWindowDownOrToWorkspaceDown {},
/// Move the focused window up in a column or to the workspace above.
MoveWindowUpOrToWorkspaceUp {},
/// Consume or expel the focused window left.
ConsumeOrExpelWindowLeft {},
/// Consume or expel the focused window right.
ConsumeOrExpelWindowRight {},
/// Consume or expel a window left.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window left")
)]
ConsumeOrExpelWindowLeft {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume or expel a window right.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window right")
)]
ConsumeOrExpelWindowRight {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right.
SwapWindowRight {},
/// Swap focused window with one to the left.
SwapWindowLeft {},
/// Toggle the focused column between normal and tabbed display.
ToggleColumnTabbedDisplay {},
/// Set the display mode of the focused column.
SetColumnDisplay {
/// Display mode to set.
#[cfg_attr(feature = "clap", arg())]
display: ColumnDisplay,
},
/// Center the focused column on the screen.
CenterColumn {},
/// Center a window on the screen.
#[cfg_attr(
feature = "clap",
clap(about = "Center the focused window on the screen")
)]
CenterWindow {
/// Id of the window to center.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Center all fully visible columns on the screen.
CenterVisibleColumns {},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -281,21 +459,94 @@ pub enum Action {
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the moved window.
///
/// If `true` (the default) and the window to move is focused, the focus will follow the
/// window to the new workspace. If `false`, the focus will remain on the original
/// workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown {},
MoveColumnToWorkspaceDown {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp {},
MoveColumnToWorkspaceUp {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused workspace down.
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp {},
/// Move a workspace to a specific index on its monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific index on its monitor")
)]
MoveWorkspaceToIndex {
/// New index for the workspace.
#[cfg_attr(feature = "clap", arg())]
index: usize,
/// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Set the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Set the name of the focused workspace")
)]
SetWorkspaceName {
/// New name for the workspace.
#[cfg_attr(feature = "clap", arg())]
name: String,
/// Reference (index or name) of the workspace to name.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
workspace: Option<WorkspaceReferenceArg>,
},
/// Unset the name of a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Unset the name of the focused workspace")
)]
UnsetWorkspaceName {
/// Reference (index or name) of the workspace to unname.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg())]
reference: Option<WorkspaceReferenceArg>,
},
/// Focus the monitor to the left.
FocusMonitorLeft {},
/// Focus the monitor to the right.
@@ -304,6 +555,16 @@ pub enum Action {
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp {},
/// Focus the previous monitor.
FocusMonitorPrevious {},
/// Focus the next monitor.
FocusMonitorNext {},
/// Focus a monitor by name.
FocusMonitor {
/// Name of the output to focus.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
@@ -312,6 +573,26 @@ pub enum Action {
MoveWindowToMonitorDown {},
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp {},
/// Move the focused window to the previous monitor.
MoveWindowToMonitorPrevious {},
/// Move the focused window to the next monitor.
MoveWindowToMonitorNext {},
/// Move a window to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused window to a specific monitor")
)]
MoveWindowToMonitor {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
@@ -320,6 +601,32 @@ pub enum Action {
MoveColumnToMonitorDown {},
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp {},
/// Move the focused column to the previous monitor.
MoveColumnToMonitorPrevious {},
/// Move the focused column to the next monitor.
MoveColumnToMonitorNext {},
/// Move the focused column to a specific monitor.
MoveColumnToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
},
/// Change the width of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Change the width of the focused window")
)]
SetWindowWidth {
/// Id of the window whose width to set.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the width.
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Change the height of a window.
#[cfg_attr(
feature = "clap",
@@ -333,7 +640,7 @@ pub enum Action {
id: Option<u64>,
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Reset the height of a window back to automatic.
@@ -350,6 +657,14 @@ pub enum Action {
},
/// Switch between preset column widths.
SwitchPresetColumnWidth {},
/// Switch between preset window widths.
SwitchPresetWindowWidth {
/// Id of the window whose width to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
@@ -363,9 +678,11 @@ pub enum Action {
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg())]
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
change: SizeChange,
},
/// Expand the focused column to space not taken up by other fully visible columns.
ExpandColumnToAvailableWidth {},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
@@ -382,12 +699,147 @@ pub enum Action {
MoveWorkspaceToMonitorDown {},
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp {},
/// Move the focused workspace to the previous monitor.
MoveWorkspaceToMonitorPrevious {},
/// Move the focused workspace to the next monitor.
MoveWorkspaceToMonitorNext {},
/// Move a workspace to a specific monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused workspace to a specific monitor")
)]
MoveWorkspaceToMonitor {
/// The target output name.
#[cfg_attr(feature = "clap", arg())]
output: String,
// Reference (index or name) of the workspace to move.
///
/// If `None`, uses the focused workspace.
#[cfg_attr(feature = "clap", arg(long))]
reference: Option<WorkspaceReferenceArg>,
},
/// Toggle a debug tint on windows.
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage {},
/// Move the focused window between the floating and the tiling layout.
ToggleWindowFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the floating layout.
MoveWindowToFloating {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Move the focused window to the tiling layout.
MoveWindowToTiling {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switches focus to the floating layout.
FocusFloating {},
/// Switches focus to the tiling layout.
FocusTiling {},
/// Toggles the focus between the floating and the tiling layout.
SwitchFocusBetweenFloatingAndTiling {},
/// Move a floating window on screen.
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
MoveFloatingWindow {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the X position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
x: PositionChange,
/// How to change the Y position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
y: PositionChange,
},
/// Toggle the opacity of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle the opacity of the focused window")
)]
ToggleWindowRuleOpacity {
/// Id of the window.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a window.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused window")
)]
SetDynamicCastWindow {
/// Id of the window to target.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused monitor")
)]
SetDynamicCastMonitor {
/// Name of the output to target.
///
/// If `None`, uses the focused output.
#[cfg_attr(feature = "clap", arg())]
output: Option<String>,
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle (open/close) the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
/// Toggle urgent status of a window.
ToggleWindowUrgent {
/// Id of the window to toggle urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Set urgent status of a window.
SetWindowUrgent {
/// Id of the window to set urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Unset urgent status of a window.
UnsetWindowUrgent {
/// Id of the window to unset urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
}
/// Change in window or column size.
@@ -404,6 +856,16 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Change in floating window position.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum PositionChange {
/// Set the position in logical pixels.
SetFixed(f64),
/// Add or subtract to the current position in logical pixels.
AdjustFixed(f64),
}
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -424,6 +886,18 @@ pub enum LayoutSwitchTarget {
Next,
/// The previous configured layout.
Prev,
/// The specific layout by index.
Index(u8),
}
/// How windows display in a column.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ColumnDisplay {
/// Windows are tiled vertically across the working area height.
Normal,
/// Windows are in tabs.
Tabbed,
}
/// Output actions that niri can perform.
@@ -659,12 +1133,23 @@ pub struct Window {
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
/// Process ID that created the Wayland connection for this window, if known.
///
/// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
/// change in the future.
pub pid: Option<i32>,
/// Id of the workspace this window is on, if any.
pub workspace_id: Option<u64>,
/// Whether this window is currently focused.
///
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
pub is_focused: bool,
/// Whether this window is currently floating.
///
/// If the window isn't floating then it is in the tiling layout.
pub is_floating: bool,
/// Whether this window requests your attention.
pub is_urgent: bool,
}
/// Output configuration change result.
@@ -704,6 +1189,8 @@ pub struct Workspace {
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace currently has an urgent window in its output.
pub is_urgent: bool,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
@@ -726,6 +1213,46 @@ pub struct KeyboardLayouts {
pub current_idx: u8,
}
/// A layer-shell layer.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum Layer {
/// The background layer.
Background,
/// The bottom layer.
Bottom,
/// The top layer.
Top,
/// The overlay layer.
Overlay,
}
/// Keyboard interactivity modes for a layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum LayerSurfaceKeyboardInteractivity {
/// Surface cannot receive keyboard focus.
None,
/// Surface receives keyboard focus whenever possible.
Exclusive,
/// Surface receives keyboard focus on demand, e.g. when clicked.
OnDemand,
}
/// A layer-shell surface.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct LayerSurface {
/// Namespace provided by the layer-shell client.
pub namespace: String,
/// Name of the output the surface is on.
pub output: String,
/// Layer that the surface is on.
pub layer: Layer,
/// The surface's keyboard interactivity mode.
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -738,6 +1265,13 @@ pub enum Event {
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// The workspace urgency changed.
WorkspaceUrgencyChanged {
/// Id of the workspace.
id: u64,
/// Whether this workspace has an urgent window.
urgent: bool,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
@@ -785,6 +1319,13 @@ pub enum Event {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// Window urgency changed.
WindowUrgencyChanged {
/// Id of the window.
id: u64,
/// The new urgency state of the window.
urgent: bool,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
@@ -795,6 +1336,11 @@ pub enum Event {
/// Index of the newly active layout.
idx: u8,
},
/// The overview was opened or closed.
OverviewOpenedOrClosed {
/// The new state of the overview.
is_open: bool,
},
}
impl FromStr for WorkspaceReferenceArg {
@@ -855,6 +1401,25 @@ impl FromStr for SizeChange {
}
}
impl FromStr for PositionChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
@@ -862,7 +1427,22 @@ impl FromStr for LayoutSwitchTarget {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
other => match other.parse() {
Ok(layout) => Ok(Self::Index(layout)),
_ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
},
}
}
}
impl FromStr for ColumnDisplay {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"tabbed" => Ok(Self::Tabbed),
_ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
}
}
}
+42 -18
View File
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "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,
stream: BufReader<UnixStream>,
}
impl Socket {
@@ -37,6 +37,7 @@ impl Socket {
/// 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())?;
let stream = BufReader::new(stream);
Ok(Self { stream })
}
@@ -47,31 +48,54 @@ impl Socket {
/// * `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;
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
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.push('\n');
self.stream.get_mut().write_all(buf.as_bytes())?;
buf.clear();
reader.read_line(&mut buf)?;
self.stream.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
Ok(reply)
}
let events = move || {
/// Starts reading event stream [`Event`]s from the socket.
///
/// The returned function will block until the next [`Event`] arrives, then return it.
///
/// Use this only after requesting an [`EventStream`][Request::EventStream].
///
/// # Examples
///
/// ```no_run
/// use niri_ipc::{Request, Response};
/// use niri_ipc::socket::Socket;
///
/// fn main() -> std::io::Result<()> {
/// let mut socket = Socket::connect()?;
///
/// let reply = socket.send(Request::EventStream)?;
/// if matches!(reply, Ok(Response::Handled)) {
/// let mut read_event = socket.read_events();
/// while let Ok(event) = read_event() {
/// println!("Received event: {event:?}");
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
let Self { mut stream } = self;
let _ = stream.get_mut().shutdown(Shutdown::Write);
let mut buf = String::new();
move || {
buf.clear();
reader.read_line(&mut buf)?;
stream.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}
}
+45
View File
@@ -40,6 +40,9 @@ pub struct EventStreamState {
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
/// State of the overview.
pub overview: OverviewState,
}
/// The workspaces state communicated over the event stream.
@@ -63,12 +66,20 @@ pub struct KeyboardLayoutsState {
pub keyboard_layouts: Option<KeyboardLayouts>,
}
/// The overview state communicated over the event stream.
#[derive(Debug, Default)]
pub struct OverviewState {
/// Whether the overview is currently open.
pub is_open: bool,
}
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.extend(self.overview.replicate());
events
}
@@ -76,6 +87,7 @@ impl EventStreamStatePart for EventStreamState {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
let event = self.overview.apply(event)?;
Some(event)
}
}
@@ -91,6 +103,13 @@ impl EventStreamStatePart for WorkspacesState {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
for ws in self.workspaces.values_mut() {
if ws.id == id {
ws.is_urgent = urgent;
}
}
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
@@ -162,6 +181,14 @@ impl EventStreamStatePart for WindowsState {
win.is_focused = Some(win.id) == id;
}
}
Event::WindowUrgencyChanged { id, urgent } => {
for win in self.windows.values_mut() {
if win.id == id {
win.is_urgent = urgent;
break;
}
}
}
event => return Some(event),
}
None
@@ -192,3 +219,21 @@ impl EventStreamStatePart for KeyboardLayoutsState {
None
}
}
impl EventStreamStatePart for OverviewState {
fn replicate(&self) -> Vec<Event> {
vec![Event::OverviewOpenedOrClosed {
is_open: self.is_open,
}]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::OverviewOpenedOrClosed { is_open } => {
self.is_open = is_open;
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.9", path = ".." }
niri-config = { version = "0.1.9", path = "../niri-config" }
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.5.1", path = ".." }
niri-config = { version = "25.5.1", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+8 -16
View File
@@ -1,15 +1,13 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientAngle {
angle: f32,
@@ -17,7 +15,7 @@ pub struct GradientAngle {
}
impl GradientAngle {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
@@ -31,20 +29,13 @@ impl TestCase for GradientAngle {
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
let delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.angle += delta.as_secs_f32() * PI;
if self.angle >= PI * 2. {
@@ -59,19 +50,20 @@ impl TestCase for GradientAngle {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+13 -17
View File
@@ -1,16 +1,14 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientArea {
progress: f32,
@@ -19,14 +17,16 @@ pub struct GradientArea {
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
urgent_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
});
Self {
@@ -43,20 +43,13 @@ impl TestCase for GradientArea {
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
let delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.progress += delta.as_secs_f32() * PI;
if self.progress >= PI * 2. {
@@ -74,8 +67,8 @@ impl TestCase for GradientArea {
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
@@ -83,16 +76,18 @@ impl TestCase for GradientArea {
));
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
let mut g_area = Rectangle::new(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update_render_elements(
g_size,
true,
true,
false,
Rectangle::default(),
CornerRadius::default(),
1.,
1.,
);
rv.extend(
self.border
@@ -108,10 +103,11 @@ impl TestCase for GradientArea {
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
Rectangle::from_size(rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklab {
gradient_format: GradientInterpolation,
}
impl GradientOklab {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklab {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklabAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklabAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
@@ -29,19 +29,20 @@ impl TestCase for GradientOklabAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklchAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchDecreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchDecreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchDecreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchIncreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchIncreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchIncreasing {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchLonger {
gradient_format: GradientInterpolation,
}
impl GradientOklchLonger {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchLonger {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientOklchShorter {
gradient_format: GradientInterpolation,
}
impl GradientOklchShorter {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchShorter {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+7 -6
View File
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgb {
gradient_format: GradientInterpolation,
}
impl GradientSrgb {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -31,19 +31,20 @@ impl TestCase for GradientSrgb {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
@@ -29,19 +29,20 @@ impl TestCase for GradientSrgbAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -4,16 +4,16 @@ use niri_config::{
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbLinear {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinear {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -31,19 +31,20 @@ impl TestCase for GradientSrgbLinear {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
pub struct GradientSrgbLinearAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinearAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
pub fn new(_args: Args) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
@@ -29,19 +29,20 @@ impl TestCase for GradientSrgbLinearAlpha {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
Rectangle::from_size(area.size),
0.,
CornerRadius::default(),
1.,
1.,
)
.with_location(area.loc)]
.into_iter()
+87 -52
View File
@@ -1,18 +1,17 @@
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::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::{Color, FloatOrInt, OutputName};
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
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 +19,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 {
@@ -58,46 +60,51 @@ impl Layout {
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_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)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
rv.layout.activate_window(&0);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
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| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -105,13 +112,13 @@ 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| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -119,44 +126,59 @@ 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)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.3)));
l.layout.start_open_animation_for_window(win.id());
});
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)));
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.5)));
l.layout.start_open_animation_for_window(win.id());
});
rv
}
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false, None);
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.communicate();
self.layout.add_window(window.clone(), width, false);
self.layout.add_window(
window.clone(),
AddWindowTarget::Auto,
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
@@ -164,14 +186,28 @@ impl Layout {
&mut self,
right_of: &TestWindow,
mut window: TestWindow,
width: Option<ColumnWidth>,
width: Option<PresetSize>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false, None);
let min_size = window.min_size();
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
false,
false,
None,
);
window.communicate();
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
self.layout.add_window(
window.clone(),
AddWindowTarget::NextTo(right_of.id()),
width,
None,
false,
false,
ActivateWindow::default(),
);
self.windows.push(window);
}
@@ -198,29 +234,28 @@ impl TestCase for Layout {
}
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(
@@ -232,8 +267,8 @@ impl TestCase for Layout {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _)
.collect()
}
+7 -1
View File
@@ -1,8 +1,9 @@
use std::time::Duration;
use niri::animation::Clock;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
use smithay::utils::{Logical, Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
@@ -21,6 +22,11 @@ pub mod layout;
pub mod tile;
pub mod window;
pub struct Args {
pub size: Size<i32, Logical>,
pub clock: Clock,
}
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
+37 -37
View File
@@ -6,9 +6,9 @@ use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use smithay::utils::{Physical, Point, Rectangle, Size};
use super::TestCase;
use super::{Args, TestCase};
use crate::test_window::TestWindow;
pub struct Tile {
@@ -17,53 +17,46 @@ pub struct Tile {
}
impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
pub fn freeform(args: Args) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
pub fn fixed_size(args: Args) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
Self::with_window(args, window)
}
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::freeform(size);
pub fn freeform_open(args: Args) -> Self {
let mut rv = Self::freeform(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size(size);
pub fn fixed_size_open(args: Args) -> Self {
let mut rv = Self::fixed_size(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(size);
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(args);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn with_window(window: TestWindow) -> Self {
pub fn with_window(args: Args, window: TestWindow) -> Self {
let Args { size, clock } = args;
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
@@ -77,15 +70,28 @@ impl Tile {
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
let mut tile = niri::layout::tile::Tile::new(
window.clone(),
size.to_f64(),
1.,
clock,
Rc::new(options),
);
tile.request_tile_size(size.to_f64(), false, None);
window.communicate();
Self { window, tile }
}
}
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
let size = Size::from((width, height)).to_f64();
self.tile
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
.update_config(size, 1., self.tile.options().clone());
self.tile.request_tile_size(size, false, None);
self.window.communicate();
}
@@ -93,8 +99,8 @@ impl TestCase for Tile {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time);
fn advance_animations(&mut self, _current_time: Duration) {
self.tile.advance_animations();
}
fn render(
@@ -106,18 +112,12 @@ impl TestCase for Tile {
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update(
self.tile.update_render_elements(
true,
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
);
self.tile
.render(
renderer,
location,
Scale::from(1.),
true,
RenderTarget::Output,
)
.render(renderer, location, true, RenderTarget::Output)
.map(|elem| Box::new(elem) as _)
.collect()
}
+9 -9
View File
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use smithay::utils::{Physical, Point, Scale, Size};
use super::TestCase;
use super::{Args, TestCase};
use crate::test_window::TestWindow;
pub struct Window {
@@ -12,24 +12,24 @@ pub struct Window {
}
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
pub fn freeform(args: Args) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
pub fn fixed_size(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(size, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
@@ -38,7 +38,7 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window
.request_size(Size::from((width, height)), false, None);
.request_size(Size::from((width, height)), false, false, None);
self.window.communicate();
}
+7 -15
View File
@@ -2,15 +2,11 @@
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use cases::Args;
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
use gtk::{gdk, gio, glib};
use niri::animation::ANIMATION_SLOWDOWN;
use smithay::utils::{Logical, Size};
use smithay_view::SmithayView;
use tracing_subscriber::EnvFilter;
@@ -66,24 +62,23 @@ fn on_startup(_app: &adw::Application) {
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
struct S {
stack: gtk::Stack,
anim_adjustment: gtk::Adjustment,
}
impl S {
fn add<T: TestCase + 'static>(
&self,
make: impl Fn(Size<i32, Logical>) -> T + 'static,
title: &str,
) {
let view = SmithayView::new(make);
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
let view = SmithayView::new(make, &self.anim_adjustment);
self.stack.add_titled(&view, None, title);
}
}
let s = S {
stack: stack.clone(),
anim_adjustment: anim_adjustment.clone(),
};
s.add(Window::freeform, "Freeform Window");
@@ -137,9 +132,6 @@ fn build_ui(app: &adw::Application) {
let content_headerbar = adw::HeaderBar::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
anim_adjustment
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
anim_scale.set_hexpand(true);
+94 -49
View File
@@ -1,35 +1,44 @@
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use smithay::utils::{Logical, Size};
use smithay::utils::Size;
use crate::cases::TestCase;
use crate::cases::{Args, TestCase};
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use std::time::Duration;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::animation::Clock;
use niri::render_helpers::{resources, shaders};
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
struct RendererData {
renderer: GlesRenderer,
dummy_texture: GlesTexture,
}
#[derive(Default)]
pub struct SmithayView {
gl_area: gtk::GLArea,
size: Cell<(i32, i32)>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
renderer: RefCell<Option<Result<RendererData, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
pub clock: RefCell<Clock>,
}
#[glib::object_subclass]
@@ -122,30 +131,71 @@ mod imp {
let Ok(renderer) = renderer else {
return Ok(());
};
let RendererData {
renderer,
dummy_texture,
} = renderer;
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| {
case.render(renderer, Size::from(size))
// Fetch GtkGLArea's framebuffer binding.
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
}?;
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
// This call will already change the framebuffer binding (offscreen elements will bind
// intermediate textures during rendering).
let elements = case.render(renderer, Size::from(size));
// HACK: there's currently no way to "just" render into an externally bound framebuffer
// (like we have in this case). The render() call requires a valid target. So what
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
// before rendering.
let mut dummy_target = renderer
.bind(dummy_texture)
.context("error binding dummy texture")?;
let mut frame = renderer
.render(rect.size, Transform::Normal)
.render(&mut dummy_target, rect.size, Transform::Normal)
.context("error creating frame")?;
// Now that render() bound the dummy texture, change the binding underneath it back to
// GtkGLArea's framebuffer, to render there instead.
frame
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
frame
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
.context("error clearing")?;
@@ -166,7 +216,7 @@ mod imp {
}
}
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
@@ -189,40 +239,17 @@ mod imp {
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
let dummy_texture = renderer
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
.context("error creating dummy texture")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
Ok(renderer)
}
unsafe fn with_framebuffer_save_restore<T>(
renderer: &mut GlesRenderer,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> anyhow::Result<T> {
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
let rv = f(renderer);
renderer.unbind().context("error unbinding")?;
renderer
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
Ok(rv)
Ok(RendererData {
renderer,
dummy_texture,
})
}
}
@@ -233,14 +260,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
}
}
+20 -8
View File
@@ -6,12 +6,13 @@ use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::offscreen::OffscreenData;
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::backend::renderer::element::Kind;
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
@@ -181,15 +182,12 @@ impl LayoutElement for TestWindow {
fn request_size(
&mut self,
size: Size<i32, Logical>,
is_fullscreen: bool,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
}
fn min_size(&self) -> Size<i32, Logical> {
@@ -214,14 +212,20 @@ impl LayoutElement for TestWindow {
fn output_leave(&self, _output: &Output) {}
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
fn set_activated(&mut self, _active: bool) {}
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 is_ignoring_opacity_window_rule(&self) -> bool {
false
}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
@@ -240,6 +244,10 @@ impl LayoutElement for TestWindow {
self.inner.borrow().requested_size
}
fn is_child_of(&self, _parent: &Self) -> bool {
false
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
@@ -259,9 +267,13 @@ impl LayoutElement for TestWindow {
fn cancel_interactive_resize(&mut self) {}
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn on_commit(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
fn is_urgent(&self) -> bool {
false
}
}
+9 -7
View File
@@ -33,7 +33,6 @@ Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 OR BSL-1.0
@@ -41,18 +40,21 @@ SourceLicense: GPL-3.0-or-later
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
# BSD-2-Clause
# BSD-2-Clause OR Apache-2.0 OR MIT
# BSD-3-Clause
# BSD-3-Clause OR MIT OR Apache-2.0
# GPL-3.0-or-later
# ISC
# MIT
# MIT AND (MIT OR Apache-2.0)
# MIT OR Apache-2.0
# (MIT OR Apache-2.0) AND BSD-3-Clause
# (MIT OR Apache-2.0) AND Unicode-3.0
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unicode-3.0
# Unlicense OR MIT
# Zlib OR Apache-2.0 OR MIT
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
@@ -87,6 +89,7 @@ Recommends: gnome-keyring
Recommends: alacritty
Recommends: fuzzel
Recommends: swaylock
Recommends: waybar
# Suggested utilities
Recommends: swaybg
Recommends: mako
@@ -101,10 +104,6 @@ Opening a new window never causes existing windows to resize.
%prep
{{{ git_dir_setup_macro }}}
# Make the version log message look nicer: since we're building not from niri's git repository,
# the git version macro will show its fallback string.
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
%cargo_prep -N
# We're doing an online build.
@@ -113,6 +112,9 @@ sed -i 's/^offline = true$//' .cargo/config.toml
# Final step in leaving alone our debug settings.
sed -i 's/.*please-remove-me$//' .cargo/config.toml
# Set the commit string.
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
%build
%cargo_build
+122 -10
View File
@@ -1,7 +1,7 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
// Input device configuration.
// Find the full list of options on the wiki:
@@ -16,6 +16,9 @@ input {
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
@@ -25,6 +28,8 @@ input {
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
@@ -40,6 +45,16 @@ input {
// 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
@@ -177,10 +192,50 @@ layout {
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can enable drop shadows for windows.
shadow {
// Uncomment the next line to enable shadows.
// on
// By default, the shadow draws only around its window, and not behind it.
// Uncomment this setting to make the shadow draw behind its window.
//
// Note that niri has no way of knowing about the CSD window corner
// radius. It has to assume that windows have square corners, leading to
// shadow artifacts inside the CSD rounded corners. This setting fixes
// those artifacts.
//
// However, instead you may want to set prefer-no-csd and/or
// geometry-corner-radius. Then, niri will know the corner radius and
// draw the shadow correctly, without having to draw it behind the
// window. These will also remove client-side shadows if the window
// draws any.
//
// draw-behind-window true
// You can change how shadows look. The values below are in logical
// pixels and match the CSS box-shadow properties.
// Softness controls the shadow blur radius.
softness 30
// Spread expands the shadow.
spread 5
// Offset moves the shadow relative to the window.
offset x=0 y=5
// You can also change the shadow color and opacity.
color "#0007"
}
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
@@ -198,11 +253,15 @@ layout {
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// spawn-at-startup "alacritty" "-e" "fish"
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "waybar"
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// 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.
@@ -238,6 +297,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 {
@@ -250,6 +318,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
@@ -266,9 +341,9 @@ binds {
Mod+Shift+Slash { show-hotkey-overlay; }
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Super+Alt+L { spawn "swaylock"; }
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
@@ -281,6 +356,11 @@ binds {
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"; }
// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
@@ -422,20 +502,32 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }
// There are also commands that consume or expel a single window to the side.
// The following binds move the focused window in and out of a column.
// If the window is alone, they will consume it into the nearby column to the side.
// If the window is already in a column, they will expel it out.
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
// Consume one window from the right to the bottom of the focused column.
Mod+Comma { consume-window-into-column; }
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// Expand the focused column to space not taken up by other fully visible columns.
// Makes the column "fill the rest of the space".
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
// Center all fully visible columns on screen.
Mod+Ctrl+C { center-visible-columns; }
// Finer width adjustments.
// This command can also:
// * set width in pixels: "1000"
@@ -451,6 +543,15 @@ 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; }
// Toggle tabbed column display mode.
// Windows in this column will appear as vertical tabs,
// rather than stacked on top of each other.
Mod+W { toggle-column-tabbed-display; }
// Actions to switch layouts.
// Note: if you uncomment these, make sure you do NOT have
// a matching layout switch hotkey configured in xkb options above.
@@ -463,8 +564,19 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// Applications such as remote-desktop clients and software KVM switches may
// request that niri stops processing the keyboard shortcuts defined here
// so they may, for example, forward the key presses as-is to a remote machine.
// It's a good idea to bind an escape hatch to toggle the inhibitor,
// so a buggy application can't hold your session hostage.
//
// The allow-inhibiting=false property can be applied to other binds as well,
// which ensures niri always processes them, even when an inhibitor is active.
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
// 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
+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 2>&1; then
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
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 2>&1; then
# Check that the user dinit daemon is running
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
echo "dinit user daemon is not running."
exit 1
fi
# Make sure there's no already running session.
if dinitctl --user is-started niri >/dev/null 2>&1; 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 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
+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));
}
}
+82 -103
View File
@@ -2,14 +2,12 @@ use std::time::Duration;
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
mod clock;
pub use clock::Clock;
#[derive(Debug, Clone)]
pub struct Animation {
@@ -23,7 +21,7 @@ pub struct Animation {
/// Best effort; not always exactly precise.
clamped_duration: Duration,
start_time: Duration,
current_time: Duration,
clock: Clock,
kind: Kind,
}
@@ -48,11 +46,17 @@ pub enum Curve {
}
impl Animation {
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
pub fn new(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
) -> Self {
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / clock.rate().max(0.001);
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
rv.is_off = true;
return rv;
@@ -71,7 +75,6 @@ impl Animation {
}
let start_time = self.start_time;
let current_time = self.current_time;
match config.kind {
niri_config::AnimationKind::Spring(p) => {
@@ -83,10 +86,11 @@ impl Animation {
initial_velocity: self.initial_velocity,
params,
};
*self = Self::spring(spring);
*self = Self::spring(self.clock.clone(), spring);
}
niri_config::AnimationKind::Easing(p) => {
*self = Self::ease(
self.clock.clone(),
self.from,
self.to,
self.initial_velocity,
@@ -97,7 +101,6 @@ impl Animation {
}
self.start_time = start_time;
self.current_time = current_time;
}
/// Restarts the animation using the previous config.
@@ -106,11 +109,12 @@ impl Animation {
return self.clone();
}
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
// Scale the velocity by rate to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
match self.kind {
Kind::Easing { curve } => Self::ease(
self.clock.clone(),
from,
to,
initial_velocity,
@@ -119,28 +123,37 @@ impl Animation {
),
Kind::Spring(spring) => {
let spring = Spring {
from: self.from,
to: self.to,
from,
to,
initial_velocity: self.initial_velocity,
params: spring.params,
};
Self::spring(spring)
Self::spring(self.clock.clone(), spring)
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let threshold = 0.001; // FIXME
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
Self::decelerate(
self.clock.clone(),
from,
initial_velocity,
deceleration_rate,
threshold,
)
}
}
}
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
pub fn ease(
clock: Clock,
from: f64,
to: f64,
initial_velocity: f64,
duration_ms: u64,
curve: Curve,
) -> Self {
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
@@ -152,19 +165,15 @@ impl Animation {
duration,
// Our current curves never overshoot.
clamped_duration: duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn spring(spring: Spring) -> Self {
pub fn spring(clock: Clock, spring: Spring) -> Self {
let _span = tracy_client::span!("Animation::spring");
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
let kind = Kind::Spring(spring);
@@ -176,22 +185,19 @@ impl Animation {
is_off: false,
duration,
clamped_duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn decelerate(
clock: Clock,
from: f64,
initial_velocity: f64,
deceleration_rate: f64,
threshold: f64,
) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration_s = if initial_velocity == 0. {
0.
} else {
@@ -214,85 +220,43 @@ impl Animation {
is_off: false,
duration,
clamped_duration: duration,
start_time: now,
current_time: now,
start_time: clock.now(),
clock,
kind,
}
}
pub fn set_current_time(&mut self, time: Duration) {
if self.duration.is_zero() {
self.current_time = time;
return;
}
let end_time = self.start_time + self.duration;
if end_time <= self.current_time {
return;
}
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
if slowdown <= f64::EPSILON {
// Zero slowdown will cause the animation to end right away.
self.current_time = end_time;
return;
}
// We can't change current_time (since the incoming time values are always real-time), so
// apply the slowdown by shifting the start time to compensate.
if self.current_time <= time {
let delta = time - self.current_time;
let max_delta = end_time - self.current_time;
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
if slowdown <= min_slowdown {
// Our slowdown value will cause the animation to end right away.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time -= adjusted_delta - delta;
} else {
self.start_time += delta - adjusted_delta;
}
} else {
let delta = self.current_time - time;
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
if slowdown <= min_slowdown {
// Current time was about to jump to before the animation had started; let's just
// cancel the animation in this case.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time += adjusted_delta - delta;
} else {
self.start_time -= delta - adjusted_delta;
}
}
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.duration
}
pub fn is_clamped_done(&self) -> bool {
self.current_time >= self.start_time + self.clamped_duration
if self.clock.should_complete_instantly() {
return true;
}
self.clock.now() >= self.start_time + self.clamped_duration
}
pub fn value(&self) -> f64 {
if self.is_done() {
pub fn value_at(&self, at: Duration) -> f64 {
if at <= self.start_time {
// Return from when at == start_time so that when the animations are off, the behavior
// within a single event loop cycle (i.e. no time had passed since the start of an
// animation) matches the behavior when the animations are on.
return self.from;
} else if self.start_time + self.duration <= at {
return self.to;
}
let passed = self.current_time.saturating_sub(self.start_time);
if self.clock.should_complete_instantly() {
return self.to;
}
let passed = at.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
@@ -325,6 +289,10 @@ impl Animation {
}
}
pub fn value(&self) -> f64 {
self.value_at(self.clock.now())
}
/// Returns a value that stops at the target value after first reaching it.
///
/// Best effort; not always exactly precise.
@@ -340,11 +308,22 @@ impl Animation {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
pub fn start_time(&self) -> Duration {
self.start_time
}
pub fn end_time(&self) -> Duration {
self.start_time + self.duration
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn offset(&mut self, offset: f64) {
self.from += offset;
self.to += offset;
+41
View File
@@ -54,6 +54,10 @@ impl Spring {
return Duration::MAX;
}
if (self.to - self.from).abs() <= f64::EPSILON {
return Duration::ZERO;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
@@ -90,6 +94,12 @@ impl Spring {
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
// Overdamped springs have some numerical stability issues...
if !y1.is_finite() {
return Duration::from_secs_f64(x0);
}
i += 1;
}
@@ -166,3 +176,34 @@ impl Spring {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overdamped_spring_equal_from_to_nan() {
let spring = Spring {
from: 0.,
to: 0.,
initial_velocity: 0.,
params: SpringParams::new(1.15, 850., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
#[test]
fn overdamped_spring_duration_panic() {
let spring = Spring {
from: 0.,
to: 1.,
initial_velocity: 0.,
params: SpringParams::new(6., 1200., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
}
+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()
}
}
+37 -11
View File
@@ -2,12 +2,12 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, ModKey};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
@@ -17,9 +17,14 @@ pub use tty::Tty;
pub mod winit;
pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
#[allow(clippy::large_enum_variant)]
pub enum Backend {
Tty(Tty),
Winit(Winit),
Headless(Headless),
}
#[derive(PartialEq, Eq)]
@@ -54,6 +59,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.init(niri),
Backend::Winit(winit) => winit.init(niri),
Backend::Headless(headless) => headless.init(niri),
}
}
@@ -61,6 +67,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.seat_name(),
Backend::Winit(winit) => winit.seat_name(),
Backend::Headless(headless) => headless.seat_name(),
}
}
@@ -71,6 +78,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.with_primary_renderer(f),
Backend::Winit(winit) => winit.with_primary_renderer(f),
Backend::Headless(headless) => headless.with_primary_renderer(f),
}
}
@@ -83,13 +91,20 @@ 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),
}
}
pub fn mod_key(&self) -> CompositorMod {
pub fn mod_key(&self, config: &Config) -> ModKey {
match self {
Backend::Tty(_) => CompositorMod::Super,
Backend::Winit(_) => CompositorMod::Alt,
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
if let Some(ModKey::Alt) = config.input.mod_key {
ModKey::Super
} else {
ModKey::Alt
}
}),
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
}
}
@@ -97,6 +112,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.change_vt(vt),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -104,6 +120,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.suspend(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -111,6 +128,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.toggle_debug_tint(),
Backend::Winit(winit) => winit.toggle_debug_tint(),
Backend::Headless(_) => (),
}
}
@@ -118,6 +136,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
}
}
@@ -125,6 +144,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -132,6 +152,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
Backend::Headless(headless) => headless.ipc_outputs(),
}
}
@@ -143,6 +164,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
Backend::Headless(_) => None,
}
}
@@ -150,6 +172,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -157,6 +180,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -164,13 +188,7 @@ impl Backend {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
}
}
pub fn on_debug_config_changed(&mut self) {
match self {
Backend::Tty(tty) => tty.on_debug_config_changed(),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
@@ -197,4 +215,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")
}
}
}
+316 -268
View File
@@ -18,9 +18,10 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
};
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLDevice, EGLDisplay};
@@ -28,7 +29,7 @@ use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
@@ -50,6 +51,7 @@ use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlob
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::presentation::Refresh;
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
@@ -61,9 +63,14 @@ use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
Fourcc::Xrgb8888,
Fourcc::Xbgr8888,
Fourcc::Argb8888,
Fourcc::Abgr8888,
];
pub struct Tty {
config: Rc<RefCell<Config>>,
@@ -95,19 +102,20 @@ pub type TtyRenderer<'render> = MultiRenderer<
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyFrame<'render, 'frame> = MultiFrame<
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
'render,
'render,
'frame,
'buffer,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
type GbmDrmCompositor = DrmCompositor<
GbmAllocator<DrmDeviceFd>,
GbmDevice<DrmDeviceFd>,
GbmFramebufferExporter<DrmDeviceFd>,
(OutputPresentationFeedback, Duration),
DrmDeviceFd,
>;
@@ -117,7 +125,7 @@ pub struct OutputDevice {
render_node: DrmNode,
drm_scanner: DrmScanner,
surfaces: HashMap<crtc::Handle, Surface>,
output_ids: HashMap<crtc::Handle, OutputId>,
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
// SAFETY: drop after all the objects used with them are dropped.
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
@@ -128,6 +136,13 @@ pub struct OutputDevice {
active_leases: Vec<DrmLease>,
}
// A connected, but not necessarily enabled, crtc.
#[derive(Debug, Clone)]
pub struct CrtcInfo {
id: OutputId,
name: OutputName,
}
impl OutputDevice {
pub fn lease_request(
&self,
@@ -167,6 +182,35 @@ impl OutputDevice {
pub fn remove_lease(&mut self, lease_id: u32) {
self.active_leases.retain(|l| l.id() != lease_id);
}
pub fn known_crtc_name(
&self,
crtc: &crtc::Handle,
conn: &connector::Info,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
let conn_name = format_connector_name(conn);
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
}
let Some(info) = self.known_crtcs.get(crtc) else {
let conn_name = format_connector_name(conn);
error!("crtc for connector {conn_name} missing from known");
return OutputName {
connector: conn_name,
make: None,
model: None,
serial: None,
};
};
info.name.clone()
}
}
#[derive(Debug, Clone, Copy)]
@@ -183,7 +227,6 @@ struct Surface {
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
pending_gamma_change: Option<Option<Vec<u16>>>,
vrr_enabled: bool,
/// Tracy frame that goes from vblank to vblank.
vblank_frame: Option<tracy_client::Frame>,
/// Frame name for the VBlank frame.
@@ -404,8 +447,6 @@ impl Tty {
self.device_changed(node.dev_id(), niri);
// Apply pending gamma changes and restore our existing gamma.
//
// Also, restore our VRR.
let device = self.devices.get_mut(&node).unwrap();
for (crtc, surface) in device.surfaces.iter_mut() {
if let Some(ramp) = surface.pending_gamma_change.take() {
@@ -423,33 +464,6 @@ impl Tty {
warn!("error restoring gamma: {err:?}");
}
}
// Restore VRR.
let output = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == node && tty_state.crtc == *crtc
})
.cloned();
let Some(output) = output else {
error!("missing output for crtc: {crtc:?}");
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name.connector);
continue;
};
try_to_change_vrr(
&device.drm,
surface.connector,
*crtc,
surface,
output_state,
surface.vrr_enabled,
);
}
}
@@ -466,7 +480,7 @@ impl Tty {
self.refresh_ipc_outputs(niri);
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.notify_activity();
niri.monitors_active = true;
self.set_monitors_active(true);
niri.queue_redraw_all();
@@ -535,7 +549,7 @@ impl Tty {
}
drop(config);
niri.layout.update_shaders();
niri.update_shaders();
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats();
@@ -596,7 +610,7 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
output_ids: HashMap::new(),
known_crtcs: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
@@ -629,6 +643,7 @@ impl Tty {
}
};
let mut added = Vec::new();
let mut removed = Vec::new();
for event in scan_result {
match event {
@@ -636,34 +651,79 @@ impl Tty {
connector,
crtc: Some(crtc),
} => {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
let connector_name = format_connector_name(&connector);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
debug!(
"new connector: {} \"{}\"",
&name.connector,
name.format_make_model_serial(),
);
// Assign an id to this crtc.
let id = OutputId::next();
added.push((crtc, CrtcInfo { id, name }));
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
} => {
self.connector_disconnected(niri, node, crtc);
removed.push(crtc);
}
_ => (),
}
}
// FIXME: this is better done in connector_disconnected(), but currently we call that to
// turn off outputs temporarily, too. So we can't do this there.
for crtc in &removed {
self.connector_disconnected(niri, node, *crtc);
}
let Some(device) = self.devices.get_mut(&node) else {
error!("device disappeared");
return;
};
for crtc in removed {
if device.output_ids.remove(&crtc).is_none() {
if device.known_crtcs.remove(&crtc).is_none() {
error!("output ID missing for disconnected crtc: {crtc:?}");
}
}
self.refresh_ipc_outputs(niri);
for (crtc, mut info) in added {
// Make/model/serial can match exactly between different physical monitors. This doesn't
// happen often, but our Layout does not support such duplicates and will panic.
//
// As a workaround, search for duplicates, and unname the new connectors if one is
// found. Connector names are always unique.
let name = &mut info.name;
let formatted = name.format_make_model_serial_or_connector();
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
if info.name.matches(&formatted) {
let connector = mem::take(&mut name.connector);
warn!(
"new connector {connector} duplicates make/model/serial \
of existing connector {}, unnaming",
info.name.connector,
);
*name = OutputName {
connector,
make: None,
model: None,
serial: None,
};
break;
}
}
// Insert it right away so next added connector will check against this one too.
let device = self.devices.get_mut(&node).unwrap();
device.known_crtcs.insert(crtc, info);
}
// This will connect any new connectors if needed, and apply other changes, such as
// connecting back the internal laptop monitor once it becomes the only monitor left.
//
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
// above.
self.on_output_config_changed(niri);
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -749,7 +809,8 @@ impl Tty {
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
@@ -767,10 +828,6 @@ impl Tty {
return Ok(());
}
// This should be unique per CRTC connection, however currently we can call
// connector_connected() multiple times for turning the output off and on.
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
let config = self
.config
.borrow()
@@ -779,11 +836,6 @@ impl Tty {
.cloned()
.unwrap_or_default();
if config.off {
debug!("output is disabled in the config");
return Ok(());
}
for m in connector.modes() {
trace!("{m:?}");
}
@@ -811,45 +863,6 @@ impl Tty {
Err(err) => debug!("error setting max bpc: {err:?}"),
}
// Try to enable VRR if requested.
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
if enabled != vrr {
warn!("failed {} VRR", word);
}
vrr_enabled = enabled;
}
Err(err) => {
warn!("error {} VRR: {err:?}", word);
}
}
} else {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
// So that we can try it again later.
vrr_enabled = true;
}
}
} else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
let mut gamma_props = GammaProps::new(&device.drm, crtc)
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
.ok();
@@ -868,6 +881,31 @@ impl Tty {
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
// Try to enable VRR if requested.
match surface.vrr_supported(connector.handle()) {
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.use_vrr(vrr) {
warn!("error {} VRR: {err:?}", word);
}
}
Ok(VrrSupport::NotSupported) => {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector does not support it");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let _ = surface.use_vrr(false);
}
Err(err) => {
warn!("error querying for VRR support: {err:?}");
}
}
// Create GBM allocator.
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
@@ -894,23 +932,6 @@ impl Tty {
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
let config = self.config.borrow();
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
// Cursor planes have bugs on some systems.
let cursor_plane_gbm = if config.debug.disable_cursor_plane {
None
} else {
Some(device.gbm.clone())
};
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
let egl_context = renderer.as_ref().egl_context();
let render_formats = egl_context.dmabuf_render_formats();
@@ -949,15 +970,15 @@ impl Tty {
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
None,
allocator.clone(),
device.gbm.clone(),
GbmFramebufferExporter::new(device.gbm.clone()),
SUPPORTED_COLOR_FORMATS,
// This is only used to pick a good internal format, so it can use the surface's render
// formats, even though we only ever render on the primary GPU.
render_formats.clone(),
device.drm.cursor_size(),
cursor_plane_gbm.clone(),
Some(device.gbm.clone()),
);
let mut compositor = match res {
@@ -975,21 +996,17 @@ impl Tty {
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
let mut planes = surface.planes().clone();
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
None,
allocator,
device.gbm.clone(),
GbmFramebufferExporter::new(device.gbm.clone()),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
cursor_plane_gbm,
Some(device.gbm.clone()),
)
.context("error creating DRM compositor")?
}
@@ -998,7 +1015,6 @@ impl Tty {
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
compositor.use_direct_scanout(!config.debug.disable_direct_scanout);
let mut dmabuf_feedback = None;
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
@@ -1019,6 +1035,16 @@ impl Tty {
}
}
// Some buggy monitors replug upon powering off, so powering on here would prevent such
// monitors from powering off. Therefore, we avoid unconditionally powering on.
if !niri.monitors_active {
if let Err(err) = compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
let vrr_enabled = compositor.vrr_enabled();
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
@@ -1036,7 +1062,6 @@ impl Tty {
compositor,
dmabuf_feedback,
gamma_props,
vrr_enabled,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
@@ -1050,15 +1075,14 @@ impl Tty {
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
// Some buggy monitors replug upon powering off, so powering on here would prevent such
// monitors from powering off. Therefore, we avoid unconditionally powering on.
if niri.monitors_active {
// Redraw the new monitor.
niri.event_loop.insert_idle(move |state| {
state.niri.queue_redraw(&output);
// Guard against output disconnecting before the idle has a chance to run.
if state.niri.output_state.contains_key(&output) {
state.niri.queue_redraw(&output);
}
});
} else {
set_crtc_active(&device.drm, crtc, false);
}
Ok(())
@@ -1216,10 +1240,17 @@ impl Tty {
// Mark the last frame as submitted.
match surface.compositor.frame_submitted() {
Ok(Some((mut feedback, target_presentation_time))) => {
let refresh = output_state
.frame_clock
.refresh_interval()
.unwrap_or(Duration::ZERO);
let refresh = match output_state.frame_clock.refresh_interval() {
Some(refresh) => {
if output_state.frame_clock.vrr() {
Refresh::Variable(refresh)
} else {
Refresh::Fixed(refresh)
}
}
None => Refresh::Unknown,
};
// FIXME: ideally should be monotonically increasing for a surface.
let seq = meta.sequence as u64;
let mut flags = wp_presentation_feedback::Kind::Vsync
@@ -1369,9 +1400,35 @@ impl Tty {
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Overlay planes are disabled by default as they cause weird performance issues on my
// system.
let flags = {
let debug = &self.config.borrow().debug;
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
} else {
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
};
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
if debug.enable_overlay_planes {
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_direct_scanout {
flags.remove(primary_scanout_flag);
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
}
if debug.disable_cursor_plane {
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
}
flags
};
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
Ok(res) => {
let needs_sync = res.needs_sync()
|| self
@@ -1552,13 +1609,13 @@ impl Tty {
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
let mut ipc_outputs = HashMap::new();
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name.clone());
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1593,8 +1650,17 @@ impl Tty {
}
}
let vrr_supported = is_vrr_capable(&device.drm, connector.handle()) == Some(true);
let vrr_enabled = surface.map_or(false, |surface| surface.vrr_enabled);
let vrr_supported = surface
.map(|surface| {
matches!(
surface.compositor.vrr_supported(connector.handle()),
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
)
})
.unwrap_or_else(|| {
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
});
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
let logical = niri
.global_space
@@ -1605,6 +1671,12 @@ impl Tty {
})
.map(logical_output);
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
let id = id.unwrap_or_else(|| {
error!("crtc for connector {connector_name} missing from known");
OutputId::next()
});
let ipc_output = niri_ipc::Output {
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
@@ -1618,10 +1690,6 @@ impl Tty {
logical,
};
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
error!("output ID missing for crtc: {crtc:?}");
OutputId::next()
});
ipc_outputs.insert(id, ipc_output);
}
}
@@ -1659,10 +1727,9 @@ impl Tty {
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
for surface in device.surfaces.values_mut() {
if let Err(err) = surface.compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
}
@@ -1680,14 +1747,17 @@ impl Tty {
for (&crtc, surface) in device.surfaces.iter_mut() {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
if tty_state.node == node && tty_state.crtc == crtc {
try_to_change_vrr(
&device.drm,
surface.connector,
crtc,
surface,
output_state,
enable_vrr,
);
let word = if enable_vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
self.refresh_ipc_outputs(niri);
return;
}
@@ -1705,6 +1775,24 @@ impl Tty {
}
self.update_output_config_on_resume = false;
// Figure out if we should disable laptop panels.
let mut disable_laptop_panels = false;
if niri.is_lid_closed {
let config = self.config.borrow();
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
// Check if any external monitor is connected.
'outer: for device in self.devices.values() {
for (connector, _crtc) in device.drm_scanner.crtcs() {
if !is_laptop_panel(&format_connector_name(connector)) {
disable_laptop_panels = true;
break 'outer;
}
}
}
}
}
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
let mut to_disconnect = vec![];
let mut to_connect = vec![];
@@ -1717,7 +1805,7 @@ impl Tty {
.find(&surface.name)
.cloned()
.unwrap_or_default();
if config.off {
if config.off || should_disable(&surface.name.connector) {
to_disconnect.push((node, crtc));
continue;
}
@@ -1735,8 +1823,11 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
let vrr_enabled = surface.compositor.vrr_enabled();
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
let is_on_demand_vrr = config.is_vrr_on_demand();
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1758,17 +1849,20 @@ impl Tty {
continue;
};
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
|| (!is_on_demand_vrr && change_always_vrr)
{
try_to_change_vrr(
&device.drm,
connector.handle(),
crtc,
surface,
output_state,
!surface.vrr_enabled,
);
let vrr = !vrr_enabled;
let word = if vrr { "enabling" } else { "disabling" };
if let Err(err) = surface.compositor.use_vrr(vrr) {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
output_state
.frame_clock
.set_vrr(surface.compositor.vrr_enabled());
}
if change_mode {
@@ -1800,12 +1894,17 @@ impl Tty {
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output_state.frame_clock =
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
output_state.frame_clock = FrameClock::new(
Some(refresh_interval(mode)),
surface.compositor.vrr_enabled(),
);
niri.output_resized(&output);
}
}
let config = self.config.borrow();
let disable_monitor_names = config.debug.disable_monitor_names;
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
@@ -1813,22 +1912,24 @@ impl Tty {
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let config = self
.config
.borrow()
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
let config = config
.outputs
.find(&output_name)
.cloned()
.unwrap_or_default();
if !config.off {
to_connect.push((node, connector.clone(), crtc));
if !(config.off || should_disable(&output_name.connector)) {
to_connect.push((node, connector.clone(), crtc, output_name));
}
}
}
@@ -1837,7 +1938,11 @@ impl Tty {
self.connector_disconnected(niri, node, crtc);
}
for (node, connector, crtc) in to_connect {
// Sort by output name to get more predictable first focused output at initial compositor
// startup, when multiple connectors appear at once.
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
for (node, connector, crtc, _name) in to_connect {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
@@ -1846,24 +1951,12 @@ impl Tty {
self.refresh_ipc_outputs(niri);
}
pub fn on_debug_config_changed(&mut self) {
let config = self.config.borrow();
let debug = &config.debug;
let use_direct_scanout = !debug.disable_direct_scanout;
// FIXME: reload other flags if possible?
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
surface.compositor.use_direct_scanout(use_direct_scanout);
}
}
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
@@ -1872,12 +1965,15 @@ impl Tty {
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
if output_name.matches(target) {
return Some(output_name);
}
@@ -2072,9 +2168,8 @@ fn surface_dmabuf_feedback(
let surface = compositor.surface();
let planes = surface.planes();
let plane_formats = surface
.plane_info()
.formats
let primary_plane_formats = surface.plane_info().formats.clone();
let primary_or_overlay_plane_formats = primary_plane_formats
.iter()
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
.copied()
@@ -2082,7 +2177,11 @@ fn surface_dmabuf_feedback(
// We limit the scan-out trache to formats we can also render from so that there is always a
// fallback render path available in case the supplied buffer can not be scanned out directly.
let mut scanout_formats = plane_formats
let mut primary_scanout_formats = primary_plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
.intersection(&primary_formats)
.copied()
.collect::<Vec<_>>();
@@ -2090,17 +2189,32 @@ fn surface_dmabuf_feedback(
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
// buffers produce a glitched scanout if the modifier is not Linear...
if primary_render_node != surface_render_node {
scanout_formats.retain(|f| f.modifier == Modifier::Linear);
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
}
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
trace!(
"primary scanout formats: {}, overlay adds: {}",
primary_scanout_formats.len(),
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
);
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
// increase the chance of scanning out a client even with our disabled-by-default overlay
// planes.
let scanout = builder
.clone()
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
scanout_formats,
primary_scanout_formats,
)
.add_preference_tranche(
surface_render_node.dev_id(),
Some(TrancheFlags::Scanout),
primary_or_overlay_scanout_formats,
)
.build()?;
@@ -2154,17 +2268,6 @@ fn get_drm_property(
.find_map(|(handle, value)| (handle == prop).then_some(value))
}
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
return;
};
let value = property::Value::Boolean(active);
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
warn!("error setting CRTC property: {err:?}");
}
}
fn refresh_interval(mode: DrmMode) -> Duration {
let clock = mode.clock() as u64;
let htotal = mode.hsync().2 as u64;
@@ -2374,24 +2477,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
info.value_type().convert_value(value).as_boolean()
}
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
let (prop, info, _) =
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
device
.set_property(crtc, prop, value.into())
.context("error setting VRR_ENABLED property")?;
let value = get_drm_property(device, crtc, prop)
.context("VRR_ENABLED property missing after setting")?;
match info.value_type().convert_value(value) {
property::Value::UnsignedRange(value) => Ok(value == 1),
property::Value::Boolean(value) => Ok(value),
_ => bail!("wrong VRR_ENABLED property type"),
}
}
pub fn set_gamma_for_crtc(
device: &DrmDevice,
crtc: crtc::Handle,
@@ -2437,43 +2522,6 @@ pub fn set_gamma_for_crtc(
Ok(())
}
fn try_to_change_vrr(
device: &DrmDevice,
connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
enable_vrr: bool,
) {
let _span = tracy_client::span!("try_to_change_vrr");
if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name.connector
);
}
}
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
+13 -11
View File
@@ -3,7 +3,6 @@ use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
@@ -16,6 +15,7 @@ use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::Window;
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
@@ -156,7 +156,7 @@ impl Winit {
}
drop(config);
niri.layout.update_shaders();
niri.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
@@ -190,12 +190,16 @@ impl Winit {
}
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
let res = self
.damage_tracker
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
.unwrap();
let res = {
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
// FIXME: currently impossible to call due to a mutable borrow.
//
// let age = self.backend.buffer_age().unwrap();
let age = 0;
self.damage_tracker
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
.unwrap()
};
niri.update_primary_scanout_output(output, &res.states);
@@ -216,11 +220,9 @@ impl Winit {
self.backend.submit(Some(damage)).unwrap();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
let mode = output.current_mode().unwrap();
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
get_monotonic_time(),
refresh,
Refresh::Unknown,
0,
wp_presentation_feedback::Kind::empty(),
);
+11
View File
@@ -2,6 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -54,6 +55,8 @@ pub enum Sub {
},
/// Cause a panic to check if the backtraces are good.
Panic,
/// Generate shell completions.
Completions { shell: Shell },
}
#[derive(Subcommand)]
@@ -64,12 +67,18 @@ pub enum Msg {
Workspaces,
/// List open windows.
Windows,
/// List open layer-shell surfaces.
Layers,
/// Get the configured keyboard layouts.
KeyboardLayouts,
/// Print information about the focused output.
FocusedOutput,
/// Print information about the focused window.
FocusedWindow,
/// Pick a window with the mouse and print information about it.
PickWindow,
/// Pick a color from the screen with the mouse.
PickColor,
/// Perform an action.
Action {
#[command(subcommand)]
@@ -96,4 +105,6 @@ pub enum Msg {
Version,
/// Request an error from the running niri instance.
RequestError,
/// Print the overview state.
OverviewState,
}
+17 -15
View File
@@ -4,12 +4,11 @@ use std::env;
use std::fs::File;
use std::io::Read;
use std::rc::Rc;
use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
use smithay::wayland::compositor::with_states;
@@ -67,7 +66,7 @@ impl CursorManager {
let hotspot = with_states(&surface, |states| {
states
.data_map
.get::<Mutex<CursorImageAttributes>>()
.get::<CursorImageSurfaceData>()
.unwrap()
.lock()
.unwrap()
@@ -76,21 +75,24 @@ impl CursorManager {
RenderCursor::Surface { hotspot, surface }
}
CursorImageStatus::Named(icon) => self
.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
}),
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
}
}
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
self.get_cursor_with_name(icon, scale)
.map(|cursor| RenderCursor::Named {
icon,
scale,
cursor,
})
.unwrap_or_else(|| RenderCursor::Named {
icon: Default::default(),
scale,
cursor: self.get_default_cursor(scale),
})
}
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
match &self.current_cursor {
CursorImageStatus::Hidden => false,
+5 -4
View File
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::message::Header;
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{dbus_interface, MessageHeader, Task};
use zbus::{interface, Task};
use super::Start;
@@ -20,11 +21,11 @@ pub struct ScreenSaver {
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
#[interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: MessageHeader<'_>,
#[zbus(header)] hdr: Header<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
@@ -33,7 +34,7 @@ impl ScreenSaver {
hdr.sender()
);
let Ok(Some(name)) = hdr.sender() else {
let Some(name) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
+5 -4
View File
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{SerializeDict, Type, Value};
use zbus::{dbus_interface, SignalContext};
use super::Start;
@@ -33,7 +34,7 @@ pub struct WindowProperties {
pub app_id: String,
}
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
#[interface(name = "org.gnome.Shell.Introspect")]
impl Introspect {
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
@@ -52,8 +53,8 @@ impl Introspect {
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
// needed for the event stream IPC anyway).
#[dbus_interface(signal)]
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
impl Introspect {
+34 -2
View File
@@ -1,7 +1,10 @@
use std::collections::HashMap;
use std::path::PathBuf;
use zbus::dbus_interface;
use niri_ipc::PickedColor;
use zbus::fdo::{self, RequestNameFlags};
use zbus::zvariant::OwnedValue;
use zbus::{interface, zvariant};
use super::Start;
@@ -12,13 +15,14 @@ pub struct Screenshot {
pub enum ScreenshotToNiri {
TakeScreenshot { include_cursor: bool },
PickColor(async_channel::Sender<Option<PickedColor>>),
}
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,
@@ -47,6 +51,34 @@ impl Screenshot {
Ok((true, filename))
}
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
let (tx, rx) = async_channel::bounded(1);
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
warn!("error sending pick color message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
let color = match rx.recv().await {
Ok(Some(color)) => color,
Ok(None) => {
return Err(fdo::Error::Failed("no color picked".to_owned()));
}
Err(err) => {
warn!("error receiving message from niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
};
let mut result = HashMap::new();
let [r, g, b] = color.rgb;
result.insert(
"color".to_string(),
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
);
Ok(result)
}
}
impl Screenshot {
+31 -6
View File
@@ -1,5 +1,5 @@
use zbus::blocking::Connection;
use zbus::Interface;
use zbus::object_server::Interface;
use crate::niri::State;
@@ -45,12 +45,39 @@ impl DBusServers {
let mut dbus = Self::default();
if is_session_instance {
let service_channel = ServiceChannel::new(niri.display_handle.clone());
let (to_niri, from_service_channel) = calloop::channel::channel();
let service_channel = ServiceChannel::new(to_niri);
niri.event_loop
.insert_source(from_service_channel, move |event, _, state| match event {
calloop::channel::Event::Msg(new_client) => {
state.niri.insert_client(new_client);
}
calloop::channel::Event::Closed => (),
})
.unwrap();
dbus.conn_service_channel = try_start(service_channel);
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.ipc_outputs());
let (to_niri, from_display_config) = calloop::channel::channel();
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
niri.event_loop
.insert_source(from_display_config, move |event, _, state| match event {
calloop::channel::Event::Msg(new_conf) => {
for (name, conf) in new_conf {
state.modify_output_config(&name, move |output| {
if let Some(new_output) = conf {
*output = new_output;
} else {
output.off = true;
}
});
}
state.reload_output_config();
}
calloop::channel::Event::Closed => (),
})
.unwrap();
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
@@ -83,7 +110,7 @@ impl DBusServers {
dbus.conn_introspect = try_start(introspect);
#[cfg(feature = "xdp-gnome-screencast")]
if niri.pipewire.is_some() {
{
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -95,8 +122,6 @@ impl DBusServers {
.unwrap();
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}
}
+209 -81
View File
@@ -1,15 +1,21 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use smithay::utils::Size;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
use zbus::{fdo, interface};
use super::Start;
use crate::backend::IpcOutputMap;
use crate::utils::is_laptop_panel;
use crate::utils::scale::supported_scales;
pub struct DisplayConfig {
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -42,7 +48,18 @@ pub struct LogicalMonitor {
properties: HashMap<String, OwnedValue>,
}
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
// ApplyMonitorsConfig
#[derive(Deserialize, Type)]
pub struct LogicalMonitorConfiguration {
x: i32,
y: i32,
scale: f64,
transform: u32,
_is_primary: bool,
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
}
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
&self,
@@ -53,75 +70,70 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.values()
// Take only enabled outputs.
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
.map(|output| {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
let display_name = make_display_name(output, is_laptop_panel);
let mut monitors = Vec::new();
let mut logical_monitors = Vec::new();
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
for output in self.ipc_outputs.lock().unwrap().values() {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let refresh = refresh_rate as f64 / 1000.;
let mut properties = HashMap::new();
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
Mode {
id: format!("{width}x{height}@{refresh:.3}"),
width: i32::from(width),
height: i32::from(height),
refresh_rate: refresh,
preferred_scale: 1.,
supported_scales: vec![1., 2., 3.],
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
modes[output.current_mode.unwrap()]
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let width = i32::from(width);
let height = i32::from(height);
let refresh_rate = refresh_rate as f64 / 1000.;
Mode {
id: format!("{width}x{height}@{refresh_rate:.3}"),
width,
height,
refresh_rate,
preferred_scale: 1.,
supported_scales: supported_scales(Size::from((width, height))).collect(),
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
if let Some(mode) = output.current_mode {
modes[mode]
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
}
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let monitor = Monitor {
names: (connector, make, model, serial),
modes,
properties,
};
let logical = output.logical.as_ref().unwrap();
let names = (connector, make, model, serial);
if let Some(logical) = output.logical.as_ref() {
let transform = match logical.transform {
niri_ipc::Transform::Normal => 0,
niri_ipc::Transform::_90 => 1,
@@ -133,35 +145,151 @@ impl DisplayConfig {
niri_ipc::Transform::Flipped270 => 7,
};
let logical_monitor = LogicalMonitor {
logical_monitors.push(LogicalMonitor {
x: logical.x,
y: logical.y,
scale: logical.scale,
transform,
is_primary: false,
monitors: vec![monitor.names.clone()],
monitors: vec![names.clone()],
properties: HashMap::new(),
};
});
}
(monitor, logical_monitor)
})
.collect();
monitors.push(Monitor {
names,
modes,
properties,
});
}
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
async fn apply_monitors_config(
&self,
_serial: u32,
method: u32,
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
_properties: HashMap<String, OwnedValue>,
) -> fdo::Result<()> {
let current_conf = self.ipc_outputs.lock().unwrap();
let mut new_conf = HashMap::new();
for requested_config in logical_monitor_configs {
if requested_config.monitors.len() > 1 {
return Err(zbus::fdo::Error::Failed(
"Mirroring is not yet supported".to_owned(),
));
}
for (connector, mode, _props) in requested_config.monitors {
if !current_conf.values().any(|o| o.name == connector) {
return Err(zbus::fdo::Error::Failed(format!(
"Connector '{}' not found",
connector
)));
}
new_conf.insert(
connector.clone(),
Some(niri_config::Output {
off: false,
name: connector,
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
transform: match requested_config.transform {
0 => niri_ipc::Transform::Normal,
1 => niri_ipc::Transform::_90,
2 => niri_ipc::Transform::_180,
3 => niri_ipc::Transform::_270,
4 => niri_ipc::Transform::Flipped,
5 => niri_ipc::Transform::Flipped90,
6 => niri_ipc::Transform::Flipped180,
7 => niri_ipc::Transform::Flipped270,
x => {
return Err(zbus::fdo::Error::Failed(format!(
"Unknown transform {}",
x
)))
}
},
position: Some(niri_config::Position {
x: requested_config.x,
y: requested_config.y,
}),
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
zbus::fdo::Error::Failed(format!(
"Could not parse mode '{}': {}",
mode, e
))
})?),
// FIXME: VRR
..Default::default()
}),
);
}
}
if new_conf.is_empty() {
return Err(zbus::fdo::Error::Failed(
"At least one output must be enabled".to_owned(),
));
}
for output in current_conf.values() {
if !new_conf.contains_key(&output.name) {
new_conf.insert(output.name.clone(), None);
}
}
if method == 0 {
// 0 means "verify", so don't actually apply here
return Ok(());
}
if let Err(err) = self.to_niri.send(new_conf) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(())
}
#[zbus(signal)]
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
#[zbus(property)]
fn power_save_mode(&self) -> i32 {
-1
}
#[zbus(property)]
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
Err(zbus::Error::Unsupported)
}
#[zbus(property)]
fn panel_orientation_managed(&self) -> bool {
false
}
#[zbus(property)]
fn apply_monitors_config_allowed(&self) -> bool {
true
}
#[zbus(property)]
fn night_light_supported(&self) -> bool {
false
}
}
impl DisplayConfig {
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
pub fn new(
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
) -> Self {
Self {
to_niri,
ipc_outputs,
}
}
}
@@ -211,16 +339,16 @@ fn format_diagonal(diagonal_inches: f64) -> String {
#[cfg(test)]
mod tests {
use k9::snapshot;
use insta::assert_snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
snapshot!(format_diagonal(12.11), "12.1″");
snapshot!(format_diagonal(13.28), "13.3″");
snapshot!(format_diagonal(15.6), "15.6″");
snapshot!(format_diagonal(23.2), "23″");
snapshot!(format_diagonal(24.8), "25″");
assert_snapshot!(format_diagonal(12.11), @"12.1″");
assert_snapshot!(format_diagonal(13.28), @"13.3″");
assert_snapshot!(format_diagonal(15.6), @"15.6″");
assert_snapshot!(format_diagonal(23.2), @"23″");
assert_snapshot!(format_diagonal(24.8), @"25″");
}
}
+43 -26
View File
@@ -5,8 +5,9 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use zbus::fdo::RequestNameFlags;
use zbus::object_server::{InterfaceRef, SignalEmitter};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use zbus::{fdo, interface, ObjectServer};
use super::Start;
use crate::backend::IpcOutputMap;
@@ -61,6 +62,8 @@ static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct Stream {
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
@@ -92,16 +95,17 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
stream_id: usize,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
signal_ctx: SignalEmitter<'static>,
},
StopCast {
session_id: usize,
},
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
#[interface(name = "org.gnome.Mutter.ScreenCast")]
impl ScreenCast {
async fn create_session(
&self,
@@ -136,26 +140,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(iface.signal_emitter().clone());
}
}
pub async fn stop(
&self,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
) {
debug!("stop");
@@ -175,7 +179,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();
}
@@ -203,16 +207,20 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let target = StreamTarget::Output(output);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -236,10 +244,8 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(?properties, "record_window");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
@@ -247,7 +253,13 @@ impl Session {
let target = StreamTarget::Window {
id: properties.window_id,
};
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
let stream = Stream::new(
stream_id,
self.id,
target,
cursor_mode,
self.to_niri.clone(),
);
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -264,17 +276,17 @@ impl Session {
Ok(path)
}
#[dbus_interface(signal)]
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
#[zbus(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
#[zbus(property)]
async fn parameters(&self) -> StreamParameters {
match &self.target {
StreamTarget::Output(output) => {
@@ -349,11 +361,15 @@ impl Drop for Session {
impl Stream {
fn new(
id: usize,
session_id: usize,
target: StreamTarget,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
session_id,
target,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
@@ -361,13 +377,14 @@ impl Stream {
}
}
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
fn start(&self, ctxt: SignalEmitter<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
let msg = ScreenCastToNiri::StartCast {
session_id,
session_id: self.session_id,
stream_id: self.id,
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
+20 -19
View File
@@ -1,50 +1,51 @@
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;
use crate::niri::NewClient;
pub struct ServiceChannel {
display: DisplayHandle,
to_niri: calloop::channel::Sender<NewClient>,
}
#[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(),
));
}
let (sock1, sock2) = UnixStream::pair().unwrap();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
let client = NewClient {
client: sock2,
restricted: false,
});
self.display.insert_client(sock2, data).unwrap();
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
// FIXME: maybe you can get the PID from D-Bus somehow?
credentials_unknown: true,
};
if let Err(err) = self.to_niri.send(client) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
}
}
impl ServiceChannel {
pub fn new(display: DisplayHandle) -> Self {
Self { display }
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
Self { to_niri }
}
}
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()?;
+167 -66
View File
@@ -1,7 +1,8 @@
use std::collections::hash_map::Entry;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::input::pointer::CursorImageStatus;
use niri_ipc::PositionChange;
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -18,9 +19,11 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::niri::{ClientState, State};
use crate::utils::send_scale_transform;
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
use crate::layout::{ActivateWindow, AddWindowTarget};
use crate::niri::{CastTarget, ClientState, LockState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{is_mapped, send_scale_transform};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
@@ -75,25 +78,25 @@ impl CompositorHandler for State {
if surface == &root_surface {
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
if is_mapped(surface) {
// The toplevel got mapped.
let Unmapped { window, state } = entry.remove();
let Unmapped {
window,
state,
activation_token_data,
} = entry.remove();
window.on_commit();
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, is_full_width, output, workspace_name) =
let (rules, width, height, is_full_width, output, workspace_id) =
if let InitialConfigureState::Configured {
rules,
width,
height,
floating_width: _,
floating_height: _,
is_full_width,
output,
workspace_name,
@@ -104,15 +107,48 @@ impl CompositorHandler for State {
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
// Check that the workspace still exists.
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
let workspace_id = workspace_name
.as_deref()
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
.map(|(_, ws)| ws.id());
(rules, width, is_full_width, output, workspace_name)
(rules, width, height, is_full_width, output, workspace_id)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None, None)
(ResolvedWindowRules::empty(), None, None, false, None, None)
};
// The GTK about dialog sets min/max size after the initial configure but
// before mapping, so we need to compute open_floating at the last possible
// moment, that is here.
let is_floating = rules.compute_open_floating(toplevel);
// Figure out if we should activate the window.
let activate = rules.open_focused.map(|focus| {
if focus {
ActivateWindow::Yes
} else {
ActivateWindow::No
}
});
let activate = activate.unwrap_or_else(|| {
// Check the token timestamp again in case the window took a while between
// requesting activation and mapping.
let token = activation_token_data.filter(|token| {
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
});
if token.is_some() {
ActivateWindow::Yes
} else {
let config = self.niri.config.borrow();
if config.debug.strict_new_window_focus_policy {
ActivateWindow::No
} else {
ActivateWindow::Smart
}
}
});
let parent = toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
@@ -123,7 +159,9 @@ impl CompositorHandler for State {
// None. If the configured output is set, that means it was set explicitly
// by a window rule or a fullscreen request.
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
parent_output.is_none()
|| output.is_none()
|| output.as_ref() == *parent_output
})
.map(|(mapped, _)| mapped.window.clone());
@@ -133,34 +171,34 @@ impl CompositorHandler for State {
let mapped = Mapped::new(window, rules, hook);
let window = mapped.window.clone();
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(workspace_name) = &workspace_name {
self.niri.layout.add_window_to_named_workspace(
workspace_name,
mapped,
width,
is_full_width,
)
let target = if let Some(p) = &parent {
// Open dialogs next to their parent window.
AddWindowTarget::NextTo(p)
} else if let Some(id) = workspace_id {
AddWindowTarget::Workspace(id)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
AddWindowTarget::Output(output)
} else {
self.niri.layout.add_window(mapped, width, is_full_width)
AddWindowTarget::Auto
};
let output = self.niri.layout.add_window(
mapped,
target,
width,
height,
is_full_width,
is_floating,
activate,
);
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
let new_focus = self.niri.layout.focus().map(|m| &m.window);
if new_focus == Some(&window) {
// We activated the newly opened window.
self.maybe_warp_cursor_to_focus();
self.niri.layer_shell_on_demand_focus = None;
}
self.niri.queue_redraw(&output);
@@ -180,18 +218,12 @@ impl CompositorHandler for State {
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
let is_mapped = is_mapped(surface);
// Must start the close animation before window.on_commit().
let transaction = Transaction::new();
@@ -210,14 +242,11 @@ impl CompositorHandler for State {
// The toplevel got unmapped.
//
// Test client: wleird-unmap.
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: id.get(),
});
.stop_casts_for_target(CastTarget::Window { id: id.get() });
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface);
@@ -237,18 +266,27 @@ impl CompositorHandler for State {
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
let serial = with_states(surface, |states| {
let (serial, buffer_delta) = with_states(surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
role.configure_serial
(role.configure_serial, buffer_delta)
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
@@ -257,10 +295,25 @@ impl CompositorHandler for State {
// The toplevel remains mapped.
self.niri.layout.update_window(&window, serial);
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
// 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,
);
}
}
self.niri.queue_redraw(&output);
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
@@ -271,10 +324,12 @@ impl CompositorHandler for State {
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
window.on_commit();
self.niri.layout.update_window(&window, None);
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
return;
}
@@ -297,29 +352,75 @@ impl CompositorHandler for State {
&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(&root_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() == &root_surface {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
self.niri.maybe_continue_to_locking();
} else {
self.niri.queue_redraw(&output.clone());
return;
}
return;
}
}
}
// This message can trigger for lock surfaces that had a commit right after we unlocked
// the session, but that's ok, we don't need to handle them.
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
}
fn destroyed(&mut self, surface: &WlSurface) {
+50 -10
View File
@@ -1,4 +1,3 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
@@ -11,8 +10,9 @@ use smithay::wayland::shell::wlr_layer::{
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;
use crate::utils::{is_mapped, output_size, send_scale_transform};
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -60,6 +60,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
@@ -118,16 +119,38 @@ impl State {
.unwrap();
if initial_configure_sent {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
if is_mapped(surface) {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let config = self.niri.config.borrow();
let rules = &config.layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let output_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
let mapped = MappedLayer::new(
layer.clone(),
rules,
output_size,
scale,
self.niri.clock.clone(),
&config,
);
let prev = self
.niri
.mapped_layer_surfaces
.insert(layer.clone(), mapped);
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
@@ -151,7 +174,24 @@ impl State {
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
let was_mapped = self.niri.mapped_layer_surfaces.remove(layer).is_some();
self.niri.unmapped_layer_surfaces.insert(surface.clone());
// After layer surface unmaps it has to perform the initial commit-configure
// sequence again. This is a workaround until Smithay properly resets
// initial_configure_sent upon the surface unmapping itself as it does for
// toplevels.
if was_mapped {
with_states(surface, |states| {
let mut data = states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap();
data.initial_configure_sent = false;
});
}
}
} else {
let scale = output.current_scale();
+272 -56
View File
@@ -7,12 +7,15 @@ 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::backend::input::{InputEvent, 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};
@@ -22,8 +25,8 @@ 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::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,
@@ -32,8 +35,11 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::keyboard_shortcuts_inhibit::{
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
@@ -41,10 +47,15 @@ use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
};
use smithay::wayland::selection::ext_data_control::{
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
};
use smithay::wayland::selection::primary_selection::{
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
};
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
use smithay::wayland::selection::wlr_data_control::{
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
};
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
@@ -55,16 +66,18 @@ use smithay::wayland::xdg_activation::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, State};
use crate::layout::ActivateWindow;
use crate::niri::{DndIcon, NewClient, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
@@ -72,12 +85,19 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::utils::{output_size, send_scale_transform};
use crate::protocols::virtual_pointer::{
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
VirtualPointerMotionEvent,
};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
@@ -135,11 +155,66 @@ impl TabletSeatHandler for State {
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_visibility.is_visible() {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
}
}
delegate_pointer_constraints!(State);
@@ -183,7 +258,28 @@ impl InputMethodHandler for State {
}
}
impl KeyboardShortcutsInhibitHandler for State {
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
&mut self.niri.keyboard_shortcuts_inhibit_state
}
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
inhibitor.activate();
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.insert(inhibitor.wl_surface().clone(), inhibitor);
}
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.remove(&inhibitor.wl_surface().clone());
}
}
delegate_input_method_manager!(State);
delegate_keyboard_shortcuts_inhibit!(State);
delegate_virtual_keyboard_manager!(State);
impl SelectionHandler for State {
@@ -225,12 +321,63 @@ 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}");
// End DnD before activating a specific window below so that it takes precedence.
self.niri.layout.dnd_end();
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
// example. On successful drop, additionally activate the target window.
let mut activate_output = true;
if let Some(target) = validated.then_some(target).flatten() {
let root = self.niri.find_root_shell_surface(&target);
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
activate_output = false;
}
}
if activate_output {
// Find the output from cursor coordinates.
//
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
// and if it comes from touch, then what the coordinates are. Need to pass more
// parameters from Smithay I guess.
//
// Assume that hidden pointer means touch DnD.
if self.niri.pointer_visibility.is_visible() {
// We can't even get the current pointer location because it's locked (we're deep
// in the grab call stack here). So use the last known one.
if let Some(output) = &self.niri.pointer_contents.output {
self.niri.layout.focus_output(output);
}
}
}
self.niri.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
@@ -248,14 +395,22 @@ impl PrimarySelectionHandler for State {
}
delegate_primary_selection!(State);
impl DataControlHandler for State {
fn data_control_state(&self) -> &DataControlState {
&self.niri.data_control_state
impl WlrDataControlHandler for State {
fn data_control_state(&self) -> &WlrDataControlState {
&self.niri.wlr_data_control_state
}
}
delegate_data_control!(State);
impl ExtDataControlHandler for State {
fn data_control_state(&self) -> &ExtDataControlState {
&self.niri.ext_data_control_state
}
}
delegate_ext_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
@@ -296,6 +451,8 @@ impl SessionLockHandler for State {
fn unlock(&mut self) {
self.niri.unlock();
self.niri.activate_monitors(&mut self.backend);
self.niri.notify_activity();
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
@@ -329,18 +486,12 @@ impl SecurityContextHandler for State {
self.niri
.event_loop
.insert_source(source, move |client, _, state| {
let config = state.niri.config.borrow();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
trace!("inserting a new restricted client, context={context:?}");
state.niri.insert_client(NewClient {
client,
restricted: true,
credentials_unknown: false,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
})
.unwrap();
}
@@ -388,22 +539,25 @@ 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;
}
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
}
}
@@ -448,6 +602,31 @@ impl ScreencopyHandler for State {
}
delegate_screencopy!(State);
impl VirtualPointerHandler for State {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
&mut self.niri.virtual_pointer_state
}
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
}
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
self.process_input_event(
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
);
}
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
}
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
}
}
delegate_virtual_pointer!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
self.backend
@@ -528,41 +707,76 @@ impl GammaControlHandler for State {
}
delegate_gamma_control!(State);
struct UrgentOnlyMarker;
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.
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
// common client behavior.
//
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
let Some((serial, seat)) = data.serial else {
return false;
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
return true;
};
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)
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
// upon clicking on their tray icon or on their notification. This debug flag makes that
// work.
//
// Clicking on a notification sends clients a perfectly valid activation token from the
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
// and we can remove this debug flag.
let config = self.niri.config.borrow();
if config.debug.honor_xdg_activation_with_invalid_serial {
return true;
}
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
// with no keyboard interactivity won't have any keyboard focus.
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
return true;
}
false
}
fn request_activation(
&mut self,
_token: XdgActivationToken,
token: XdgActivationToken,
token_data: XdgActivationTokenData,
surface: WlSurface,
) {
if token_data.timestamp.elapsed().as_secs() < 10 {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&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();
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
mapped.set_urgent(true);
self.niri.queue_redraw_all();
} else {
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);
@@ -584,3 +798,5 @@ delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
delegate_single_pixel_buffer!(State);
+350 -140
View File
@@ -1,6 +1,7 @@
use std::cell::Cell;
use calloop::Interest;
use niri_config::PresetSize;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -23,12 +24,13 @@ use smithay::wayland::compositor::{
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::selection::data_device::DnDGrab;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::{self, Layer};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
@@ -36,12 +38,17 @@ use smithay::{
};
use tracing::field::Empty;
use crate::input::move_grab::MoveGrab;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::input::touch_move_grab::TouchMoveGrab;
use crate::input::touch_resize_grab::TouchResizeGrab;
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
use crate::layout::ActivateWindow;
use crate::niri::{CastTarget, PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::utils::{
get_monotonic_time, output_matches_name, send_scale_transform, update_tiled_state, ResizeEdge,
};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -65,8 +72,98 @@ impl XdgShellHandler for State {
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
let wl_surface = surface.wl_surface();
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
pointer.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Pointer(start_data.clone()));
}
}
}
}
});
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
touch.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Touch(start_data.clone()));
}
}
}
}
});
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let Some(output) = output else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output_pos = self
.niri
.global_space
.output_geometry(&output)
.unwrap()
.loc
.to_f64();
let pos_within_output = start_data.location() - output_pos;
if !self
.niri
.layout
.interactive_move_begin(window.clone(), &output, pos_within_output)
{
return;
}
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchMoveGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
self.niri.queue_redraw(&output);
}
fn resize_request(
@@ -76,24 +173,39 @@ impl XdgShellHandler for State {
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
if pointer.has_grab(serial) {
if let Some(start_data) = pointer.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
}
}
}
}
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
if touch.has_grab(serial) {
if let Some(start_data) = touch.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
}
}
}
}
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
@@ -104,8 +216,16 @@ impl XdgShellHandler for State {
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
let mut last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click gestures, so just allow it
// to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
@@ -128,14 +248,25 @@ impl XdgShellHandler for State {
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
if !self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchResizeGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
}
fn reposition_request(
@@ -165,6 +296,7 @@ impl XdgShellHandler for State {
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
trace!("ignoring popup grab because no root surface");
return;
};
@@ -173,30 +305,41 @@ impl XdgShellHandler for State {
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
trace!("ignoring popup grab because the session is locked");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
trace!("ignoring popup grab because the screenshot UI is open");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
// FIXME: popup grabs for on-demand bottom and background layers.
if let Some(layer) = layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) {
// This is a grab for a layer surface.
if let Some(mapped) = self.niri.mapped_layer_surfaces.get(layer) {
if mapped.place_within_backdrop() {
trace!("ignoring popup grab for a layer surface within overview backdrop");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
// This is a grab for a regular window; check that there's no layer surface with a
// higher input priority.
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
(l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
&& self.niri.mapped_layer_surfaces.contains_key(l)
}) {
trace!("ignoring toplevel popup grab because the overlay layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -204,38 +347,57 @@ impl XdgShellHandler for State {
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers.layers_on(Layer::Top).any(|l| {
l.cached_state().keyboard_interactivity
(l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
&& self.niri.mapped_layer_surfaces.contains_key(l)
})
{
trace!("ignoring toplevel popup grab because the top layer has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
trace!("ignoring toplevel popup grab because another window has focus");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
trace!("ignoring popup grab because no output is active");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
let mut grab = match self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
{
Ok(grab) => grab,
Err(err) => {
trace!("ignoring popup grab: {err:?}");
return;
}
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let can_receive_keyboard_focus = self
.niri
.layout
.active_output()
.and_then(|output| {
layer_map_for_output(output)
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
})
.unwrap_or(true);
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
@@ -244,16 +406,22 @@ impl XdgShellHandler for State {
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
trace!("ignoring popup grab because of current grab mismatch");
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
if can_receive_keyboard_focus {
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
}
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
self.niri.popup_grab = Some(PopupGrabState {
root,
grab,
has_keyboard_grab: can_receive_keyboard_focus,
});
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
@@ -261,7 +429,7 @@ impl XdgShellHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&surface) {
if surface.is_initial_configure_sent() {
surface.send_configure();
}
}
@@ -280,23 +448,26 @@ impl XdgShellHandler for State {
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
.find_window_and_output_mut(toplevel.wl_surface())
{
// A configure is required in response to this event regardless if there are pending
// changes.
mapped.set_needs_configure();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
&requested_output,
None,
ActivateWindow::Smart,
);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -318,7 +489,7 @@ impl XdgShellHandler for State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -332,7 +503,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
@@ -343,7 +514,7 @@ impl XdgShellHandler for State {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None, rules);
ws.configure_new_window(&unmapped.window, None, None, false, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -360,14 +531,14 @@ impl XdgShellHandler for State {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
.find_window_and_output_mut(toplevel.wl_surface())
{
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
mapped.set_needs_configure();
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
@@ -378,6 +549,9 @@ impl XdgShellHandler for State {
InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name,
@@ -401,7 +575,7 @@ impl XdgShellHandler for State {
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
@@ -416,7 +590,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = workspace_name
@@ -432,12 +606,26 @@ impl XdgShellHandler for State {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
let is_floating = rules.compute_open_floating(&toplevel);
let configure_width = if is_floating {
*floating_width
} else if *is_full_width {
Some(PresetSize::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width, rules);
let configure_height = if is_floating {
*floating_height
} else {
*height
};
ws.configure_new_window(
&unmapped.window,
configure_width,
configure_height,
is_floating,
rules,
);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -473,13 +661,11 @@ impl XdgShellHandler for State {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output = output.cloned();
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: mapped.id().get(),
});
self.niri.stop_casts_for_target(CastTarget::Window {
id: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
@@ -493,7 +679,7 @@ impl XdgShellHandler for State {
.start_close_animation_for_window(renderer, &window, blocker);
});
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let active_window = self.niri.layout.focus().map(|m| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window, transaction.clone());
@@ -509,7 +695,9 @@ impl XdgShellHandler for State {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
@@ -525,6 +713,22 @@ impl XdgShellHandler for State {
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn parent_changed(&mut self, toplevel: ToplevelSurface) {
let Some(parent) = toplevel.parent() else {
return;
};
if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
let output = output.cloned();
let window = mapped.window.clone();
if self.niri.layout.descendants_added(&window) {
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
delegate_xdg_shell!(State);
@@ -552,8 +756,14 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_configure();
if toplevel.is_initial_configure_sent() {
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
}
}
@@ -565,15 +775,21 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_configure();
if toplevel.is_initial_configure_sent() {
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
let surface = toplevel.wl_surface();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
mapped.set_needs_configure();
} else {
toplevel.send_configure();
}
}
}
}
delegate_xdg_decoration!(State);
/// Whether KDE server decorations are in use.
#[derive(Default)]
#[derive(Default, Clone)]
pub struct KdeDecorationsModeState {
server: Cell<bool>,
}
@@ -620,18 +836,6 @@ impl XdgForeignHandler for State {
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
})
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
@@ -648,7 +852,7 @@ impl State {
self.niri.is_at_startup,
);
let Unmapped { window, state } = unmapped;
let Unmapped { window, state, .. } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
@@ -689,7 +893,7 @@ impl State {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
@@ -706,11 +910,15 @@ impl State {
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let mut floating_width = None;
let mut height = None;
let mut floating_height = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
let is_floating = rules.compute_open_floating(toplevel);
// Tell the surface the preferred size and bounds for its likely output.
let ws = rules
@@ -732,34 +940,41 @@ impl State {
});
}
width = ws.resolve_default_width(rules.default_width);
width = ws.resolve_default_width(rules.default_width, false);
floating_width = ws.resolve_default_width(rules.default_width, true);
height = ws.resolve_default_height(rules.default_height, false);
floating_height = ws.resolve_default_height(rules.default_height, true);
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
let configure_width = if is_floating {
floating_width
} else if is_full_width {
Some(PresetSize::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width, &rules);
let configure_height = if is_floating { floating_height } else { height };
ws.configure_new_window(
window,
configure_width,
configure_height,
is_floating,
&rules,
);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the tiled state for the initial configure.
update_tiled_state(toplevel, config.prefer_no_csd, rules.tiled_state);
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
height,
floating_width,
floating_height,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
workspace_name: ws.and_then(|w| w.name().cloned()),
};
toplevel.send_configure();
@@ -788,16 +1003,7 @@ impl State {
if let Some(popup) = self.niri.popups.find_popup(surface) {
match popup {
PopupKind::Xdg(ref popup) => {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<XdgPopupSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if !initial_configure_sent {
if !popup.is_initial_configure_sent() {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
{
let scale = output.current_scale();
@@ -833,8 +1039,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@@ -844,19 +1050,10 @@ impl State {
}
}
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
let mut target = self.niri.layout.popup_target_rect(window);
target.loc -= get_popup_toplevel_coords(popup).to_f64();
self.position_popup_within_rect(popup, target);
@@ -876,7 +1073,20 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
let mut target = Rectangle::from_size(output_geo.size);
// Background and bottom layer popups render below the top and the overlay layer, so let's
// put them into the non-exclusive zone.
//
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
// Smithay only computes the "all layers" non-exclusive zone atm.
//
// FIXME: related to the above, top layer popups should use the "overlay layer"
// non-exclusive zone.
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
target = map.non_exclusive_zone();
}
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
@@ -921,7 +1131,7 @@ impl State {
}
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
pub fn update_reactive_popups(&self, window: &Window) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(
@@ -930,7 +1140,7 @@ impl State {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(xdg_popup, window, output);
self.unconstrain_window_popup(xdg_popup, window);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
+51
View File
@@ -0,0 +1,51 @@
use ::input as libinput;
use smithay::backend::input;
use smithay::backend::winit::WinitVirtualDevice;
use smithay::output::Output;
use crate::niri::State;
use crate::protocols::virtual_pointer::VirtualPointer;
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
type NiriDevice: NiriInputDevice;
}
impl<T: input::InputBackend> NiriInputBackend for T
where
Self::Device: NiriInputDevice,
{
type NiriDevice = Self::Device;
}
pub trait NiriInputDevice: input::Device {
// FIXME: this should maybe be per-event, not per-device,
// but it's not clear that this matters in practice?
// it might be more obvious once we implement it for libinput
fn output(&self, state: &State) -> Option<Output>;
}
impl NiriInputDevice for libinput::Device {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: Allow specifying the output per-device?
None
}
}
impl NiriInputDevice for WinitVirtualDevice {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: we should be returning the single output that the winit backend creates,
// but for now, that will cause issues because the output is normally upside down,
// so we apply Transform::Flipped180 to it and that would also cause
// the cursor position to be flipped, which is not what we want.
//
// instead, we just return None and rely on the fact that it has only one output.
// doing so causes the cursor to be placed in *global* output coordinates,
// which are not flipped, and happen to be what we want.
None
}
}
impl NiriInputDevice for VirtualPointer {
fn output(&self, _: &State) -> Option<Output> {
self.output().cloned()
}
}
+2138 -364
View File
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
gesture,
}
}
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;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
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, 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);
// 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);
}
}
+227
View File
@@ -0,0 +1,227 @@
use niri_ipc::PickedColor;
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::ButtonState;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
use crate::niri::State;
use crate::render_helpers::{render_to_vec, RenderTarget};
pub struct PickColorGrab {
start_data: PointerGrabStartData<State>,
}
impl PickColorGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_color.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
state.niri.queue_redraw_all();
}
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
let (output, pos_within_output) = data.niri.output_under(location)?;
let output = output.clone();
data.backend
.with_primary_renderer(|renderer| {
data.niri.update_render_elements(Some(&output));
let scale = Scale::from(output.current_scale().fractional_scale());
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
// at the bottom/right edges of the monitors.
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::<i32, Physical>::from((1, 1));
let elements = data.niri.render(
renderer,
&output,
false,
// This is an interactive operation so we can render without blocking out.
RenderTarget::Output,
);
let pixels = match render_to_vec(
renderer,
size,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements.iter().rev().map(|elem| {
let offset = pos.upscale(-1);
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
}),
) {
Ok(pixels) => pixels,
Err(_) => return None,
};
if pixels.len() == 4 {
let rgb = [
f64::from(pixels[0]) / 255.0,
f64::from(pixels[1]) / 255.0,
f64::from(pixels[2]) / 255.0,
];
Some(PickedColor { rgb })
} else {
error!(
"unexpected pixel data length: {} (expected 4)",
pixels.len()
);
None
}
})
.flatten()
}
}
impl PointerGrab<State> for PickColorGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_color.take() {
let color = Self::pick_color_at_point(handle.current_location(), data);
let _ = tx.send_blocking(color);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+173
View File
@@ -0,0 +1,173 @@
use smithay::backend::input::ButtonState;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Point};
use crate::niri::State;
use crate::window::Mapped;
pub struct PickWindowGrab {
start_data: PointerGrabStartData<State>,
}
impl PickWindowGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
Self { start_data }
}
fn on_ungrab(&mut self, state: &mut State) {
if let Some(tx) = state.niri.pick_window.take() {
let _ = tx.send_blocking(None);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
// Redraw to update the cursor.
state.niri.queue_redraw_all();
}
}
impl PointerGrab<State> for PickWindowGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
if event.state != ButtonState::Pressed {
return;
}
// We're handling this press, don't send the release to the window.
data.niri.suppressed_buttons.insert(event.button);
if let Some(tx) = data.niri.pick_window.take() {
let _ = tx.send_blocking(
data.niri
.window_under(handle.current_location())
.map(Mapped::id),
);
}
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
-1
View File
@@ -22,7 +22,6 @@ impl ResizeGrab {
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
+69
View File
@@ -0,0 +1,69 @@
//! Swipe gesture from scroll events.
//!
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
//! gesture events for 3+ fingers.
#[derive(Debug)]
pub struct ScrollSwipeGesture {
ongoing: bool,
vertical: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
BeginUpdate,
Update,
End,
}
impl ScrollSwipeGesture {
pub const fn new() -> Self {
Self {
ongoing: false,
vertical: false,
}
}
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
if dx == 0. && dy == 0. {
self.ongoing = false;
Action::End
} else if !self.ongoing {
self.ongoing = true;
self.vertical = dy != 0.;
Action::BeginUpdate
} else {
Action::Update
}
}
pub fn reset(&mut self) -> bool {
if self.ongoing {
self.ongoing = false;
true
} else {
false
}
}
pub fn is_vertical(&self) -> bool {
self.vertical
}
}
impl Default for ScrollSwipeGesture {
fn default() -> Self {
Self::new()
}
}
impl Action {
pub fn begin(self) -> bool {
self == Action::BeginUpdate
}
pub fn end(self) -> bool {
self == Action::End
}
}
+29 -10
View File
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
workspace_id: WorkspaceId,
gesture: GestureState,
}
@@ -27,12 +29,24 @@ enum GestureState {
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
workspace_id,
gesture,
}
}
@@ -40,17 +54,14 @@ impl SpatialMovementGrab {
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))
}
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
};
if let Some(output) = res {
state.niri.queue_redraw(&output);
}
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
@@ -82,8 +93,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
None
}
} else {
None
}
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
@@ -106,7 +125,7 @@ impl PointerGrab<State> for SpatialMovementGrab {
data.niri.queue_redraw(&output);
}
} else {
// The resize is no longer ongoing.
// The move is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
+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);
}
}
+274
View File
@@ -0,0 +1,274 @@
use std::time::Duration;
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::niri::State;
use crate::window::Mapped;
// When the touch is stationary for this much time, it becomes an interactive move.
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
pub struct TouchOverviewGrab {
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
last_location: Point<f64, Logical>,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
InteractiveMove,
}
impl TouchOverviewGrab {
pub fn new(
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
) -> Self {
Self {
last_location: start_data.location,
start_timestamp,
start_data,
output,
start_pos_within_output,
workspace_id,
workspace_matched_narrow,
window,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
match self.gesture {
GestureState::Recognizing => {
// Tap to activate.
layout.focus_output(&self.output);
// Activate the workspace if necessary.
if self.window.is_some() || self.workspace_matched_narrow {
// When activating a window, we want to activate the window's current
// workspace. Otherwise, find the workspace that we tapped on.
let ws_matches = |ws: &Workspace<Mapped>| {
if let Some(window) = &self.window {
ws.has_window(window)
} else if let Some(ws_id) = self.workspace_id {
ws.id() == ws_id
} else {
false
}
};
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
{
// The workspace could've moved to a different output in the meantime.
(*mon.output() == self.output).then_some(ws_idx)
} else {
None
};
if let Some(ws_idx) = ws_idx {
layout.toggle_overview_to_workspace(ws_idx);
}
}
if let Some(window) = self.window.as_ref() {
layout.activate_window(window);
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_end(Some(false));
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(Some(false));
}
GestureState::InteractiveMove => {
layout.interactive_move_end(self.window.as_ref().unwrap());
}
};
state.niri.queue_redraw_all();
}
}
impl TouchGrab<State> for TouchOverviewGrab {
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;
}
let timestamp = Duration::from_millis(u64::from(event.time));
let layout = &mut data.niri.layout;
// Check if we should become interactive move.
if matches!(self.gesture, GestureState::Recognizing) {
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
let passed = timestamp.saturating_sub(self.start_timestamp);
if INTERACTIVE_MOVE_THRESHOLD <= passed
&& layout.interactive_move_begin(
window.clone(),
&self.output,
self.start_pos_within_output,
)
{
self.gesture = GestureState::InteractiveMove;
}
}
}
// Check if we should become a spatial scroll.
if matches!(self.gesture, GestureState::Recognizing) {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
if c.x * c.x + c.y * c.y >= 16. * 16. {
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
self.gesture = GestureState::ViewOffset;
}
}
}
if matches!(self.gesture, GestureState::Recognizing) {
layout.workspace_switch_gesture_begin(&self.output, false);
self.gesture = GestureState::WorkspaceSwitch;
}
}
}
// Do nothing if still recognizing.
if matches!(self.gesture, GestureState::Recognizing) {
return;
}
let delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = match self.gesture {
GestureState::Recognizing => unreachable!(),
GestureState::ViewOffset => layout
.view_offset_gesture_update(-delta.x, timestamp, false)
.is_some(),
GestureState::WorkspaceSwitch => layout
.workspace_switch_gesture_update(-delta.y, timestamp, false)
.is_some(),
GestureState::InteractiveMove => {
let window = self.window.as_ref().unwrap();
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
data.niri.layout.interactive_move_update(
window,
delta,
output,
pos_within_output,
)
} else {
false
}
}
};
if ongoing {
data.niri.queue_redraw_all();
} else {
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);
}
}
+195 -37
View File
@@ -1,9 +1,13 @@
use std::io::ErrorKind;
use std::iter::Peekable;
use std::slice;
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
Response, Transform, Window,
};
use serde_json::json;
@@ -16,6 +20,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::PickWindow => Request::PickWindow,
Msg::PickColor => Request::PickColor,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
@@ -23,27 +29,39 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
},
Msg::Workspaces => Request::Workspaces,
Msg::Windows => Request::Windows,
Msg::Layers => Request::Layers,
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
Msg::OverviewState => Request::OverviewState,
};
let socket = Socket::connect().context("error connecting to the niri socket")?;
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
let (reply, mut read_event) = socket
.send(request)
.context("error communicating with niri")?;
let result = socket.send(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)
// For errors that can be caused by a version mismatch between the running niri instance and
// the niri msg CLI, we will try to fetch and compare the versions.
let check_compositor_version = match &result {
Err(err) => {
// Response JSON parsing errors.
matches!(
err.kind(),
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
)
}
_ => None,
// Error returned from niri.
Ok(Err(_)) => true,
_ => false,
};
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
// Reconnect to support older niri versions with one request per connection.
Socket::connect()
.and_then(|mut socket| socket.send(Request::Version))
.ok()
} else {
None
};
// Default SIGPIPE so that our prints don't panic on stdout closing.
@@ -51,32 +69,31 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
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.");
// 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!();
}
None => {
// Communication error, or the original request was already a version request.
// Don't add irrelevant context.
}
}
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, or the
// original request had succeeded. Don't add irrelevant context.
}
}
anyhow!(err_msg).context("niri returned an error")
})?;
let reply = result.context("error communicating with niri")?;
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
match msg {
Msg::RequestError => {
@@ -168,6 +185,69 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!();
}
}
Msg::Layers => {
let Response::Layers(mut layers) = response else {
bail!("unexpected response: expected Layers, got {response:?}");
};
if json {
let layers = serde_json::to_string(&layers).context("error formatting response")?;
println!("{layers}");
return Ok(());
}
layers.sort_by(|a, b| {
Ord::cmp(&a.output, &b.output)
.then_with(|| Ord::cmp(&a.layer, &b.layer))
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
});
let mut iter = layers.iter().peekable();
let print = |surface: &niri_ipc::LayerSurface| {
println!(" Surface:");
println!(" Namespace: \"{}\"", &surface.namespace);
let interactivity = match surface.keyboard_interactivity {
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
};
println!(" Keyboard interactivity: {interactivity}");
};
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
output: &str,
layer| {
let mut empty = true;
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
empty = false;
println!();
print(surface);
}
if empty {
println!(" (empty)\n");
} else {
println!();
}
};
while let Some(surface) = iter.peek() {
let output = &surface.output;
println!("Output \"{output}\":");
print!(" Background layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Background);
print!(" Bottom layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
print!(" Top layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Top);
print!(" Overlay layer:");
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
}
}
Msg::FocusedOutput => {
let Response::FocusedOutput(output) = response else {
bail!("unexpected response: expected FocusedOutput, got {response:?}");
@@ -185,6 +265,43 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("No output is focused.");
}
}
Msg::PickWindow => {
let Response::PickedWindow(window) = response else {
bail!("unexpected response: expected PickedWindow, got {response:?}");
};
if json {
let window = serde_json::to_string(&window).context("error formatting response")?;
println!("{window}");
return Ok(());
}
if let Some(window) = window {
print_window(&window);
} else {
println!("No window selected.");
}
}
Msg::PickColor => {
let Response::PickedColor(color) = response else {
bail!("unexpected response: expected PickedColor, got {response:?}");
};
if json {
let color = serde_json::to_string(&color).context("error formatting response")?;
println!("{color}");
return Ok(());
}
if let Some(color) = color {
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
println!("Picked color: rgb({r}, {g}, {b})",);
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
} else {
println!("No color was picked.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
@@ -285,6 +402,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("Started reading events.");
}
let mut read_event = socket.read_events();
loop {
let event = read_event().context("error reading event from niri")?;
@@ -298,6 +416,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
println!("Workspace {id}: urgency changed to {urgent}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
@@ -323,15 +444,40 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::WindowUrgencyChanged { id, urgent } => {
println!("Window {id}: urgency changed to {urgent}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
Event::OverviewOpenedOrClosed { is_open: opened } => {
println!("Overview toggled: {opened}");
}
}
}
}
Msg::OverviewState => {
let Response::OverviewState(response) = response else {
bail!("unexpected response: expected Overview, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let Overview { is_open } = response;
if is_open {
println!("Overview is open.");
} else {
println!("Overview is closed.");
}
}
}
Ok(())
@@ -435,7 +581,8 @@ fn print_output(output: Output) -> anyhow::Result<()> {
fn print_window(window: &Window) {
let focused = if window.is_focused { " (focused)" } else { "" };
println!("Window ID {}:{focused}", window.id);
let urgent = if window.is_urgent { " (urgent)" } else { "" };
println!("Window ID {}:{focused}{urgent}", window.id);
if let Some(title) = &window.title {
println!(" Title: \"{title}\"");
@@ -449,6 +596,17 @@ fn print_window(window: &Window) {
println!(" App ID: (unset)");
}
println!(
" Is floating: {}",
if window.is_floating { "yes" } else { "no" }
);
if let Some(pid) = window.pid {
println!(" PID: {pid}");
} else {
println!(" PID: (unknown)");
}
if let Some(workspace_id) = window.workspace_id {
println!(" Workspace ID: {workspace_id}");
} else {
+265 -126
View File
@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::rc::Rc;
@@ -15,18 +16,24 @@ use futures_util::io::{AsyncReadExt, BufReader};
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::input::keyboard::XkbContextHandler;
use niri_ipc::{
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
};
use smithay::desktop::layer_map_for_output;
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use crate::backend::IpcOutputMap;
use crate::input::pick_window_grab::PickWindowGrab;
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
use crate::utils::version;
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
@@ -34,7 +41,10 @@ use crate::window::Mapped;
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
pub struct IpcServer {
pub socket_path: PathBuf,
/// Path to the IPC socket.
///
/// This is `None` when creating `IpcServer` without a socket.
pub socket_path: Option<PathBuf>,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
@@ -61,31 +71,38 @@ struct EventStreamSender {
impl IpcServer {
pub fn start(
event_loop: &LoopHandle<'static, State>,
wayland_socket_name: &str,
wayland_socket_name: Option<&OsStr>,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("Ipc::start");
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
let wayland_socket_name = wayland_socket_name.to_string_lossy();
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
Ok(PostAction::Continue)
})
.unwrap();
Ok(PostAction::Continue)
})
.unwrap();
Some(socket_path)
} else {
None
};
Ok(Self {
socket_path,
@@ -120,7 +137,9 @@ impl IpcServer {
impl Drop for IpcServer {
fn drop(&mut self) {
let _ = unlink(&self.socket_path);
if let Some(socket_path) = &self.socket_path {
let _ = unlink(socket_path);
}
}
}
@@ -166,76 +185,86 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
let mut read = BufReader::new(read);
// Read a single line to allow extensibility in the future to keep reading.
BufReader::new(read)
.read_line(&mut buf)
.await
.context("error reading request")?;
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 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");
loop {
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
let mut buf = Vec::new();
let res = read.read_until(b'\n', &mut buf).await;
match res {
Ok(0) => return Ok(()),
Ok(_) => (),
// Normal client disconnection.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
Err(err) => {
return Err(err).context("error reading request");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
let request = serde_json::from_slice(&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 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:?}");
}
}
buf.clear();
serde_json::to_writer(&mut buf, &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 _,
};
streams.push(sender);
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);
}
return Ok(());
}
}
Ok(())
}
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
@@ -257,6 +286,47 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let windows = state.windows.windows.values().cloned().collect();
Response::Windows(windows)
}
Request::Layers => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let mut layers = Vec::new();
for output in state.niri.global_space.outputs() {
let name = output.name();
for surface in layer_map_for_output(output).layers() {
let layer = match surface.layer() {
Layer::Background => niri_ipc::Layer::Background,
Layer::Bottom => niri_ipc::Layer::Bottom,
Layer::Top => niri_ipc::Layer::Top,
Layer::Overlay => niri_ipc::Layer::Overlay,
};
let keyboard_interactivity =
match surface.cached_state().keyboard_interactivity {
KeyboardInteractivity::None => {
niri_ipc::LayerSurfaceKeyboardInteractivity::None
}
KeyboardInteractivity::Exclusive => {
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
}
KeyboardInteractivity::OnDemand => {
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
}
};
layers.push(niri_ipc::LayerSurface {
namespace: surface.namespace().to_owned(),
output: name.clone(),
layer,
keyboard_interactivity,
});
}
}
let _ = tx.send_blocking(layers);
});
let result = rx.recv().await;
let layers = result.map_err(|_| String::from("error getting layers info"))?;
Response::Layers(layers)
}
Request::KeyboardLayouts => {
let state = ctx.event_stream_state.borrow();
let layout = state.keyboard_layouts.keyboard_layouts.clone();
@@ -269,11 +339,52 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::PickWindow => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let pointer = state.niri.seat.get_pointer().unwrap();
let start_data = PointerGrabStartData {
focus: None,
button: 0,
location: pointer.current_location(),
};
let grab = PickWindowGrab::new(start_data);
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
// any.
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
state.niri.pick_window = Some(tx);
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
// Redraw to update the cursor.
state.niri.queue_redraw_all();
});
let result = rx.recv().await;
let id = result.map_err(|_| String::from("error getting picked window info"))?;
let window = id.and_then(|id| {
let state = ctx.event_stream_state.borrow();
state.windows.windows.get(&id.get()).cloned()
});
Response::PickedWindow(window)
}
Request::PickColor => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
state.handle_pick_color(tx);
});
let result = rx.recv().await;
let color = result.map_err(|_| String::from("error getting picked color"))?;
Response::PickedColor(color)
}
Request::Action(action) => {
let (tx, rx) = async_channel::bounded(1);
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
// Make sure some logic like workspace clean-up has a chance to run before doing
// actions.
state.niri.advance_animations();
state.do_action(action, false);
let _ = tx.send_blocking(());
});
@@ -329,6 +440,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Response::FocusedOutput(output)
}
Request::EventStream => Response::Handled,
Request::OverviewState => {
let state = ctx.event_stream_state.borrow();
let is_open = state.overview.is_open;
Response::OverviewState(Overview { is_open })
}
};
Ok(response)
@@ -362,22 +478,15 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result
}
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
}
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(),
is_urgent: mapped.is_urgent(),
})
}
@@ -385,10 +494,13 @@ 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 layouts = context.keymap().layouts();
let xkb = context.xkb().lock().unwrap();
let layouts = xkb.layouts();
KeyboardLayouts {
names: layouts.map(str::to_owned).collect(),
current_idx: context.active_layout().0 as u8,
names: layouts
.map(|layout| xkb.layout_name(layout).to_owned())
.collect(),
current_idx: xkb.active_layout().0 as u8,
}
});
@@ -406,7 +518,10 @@ impl State {
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| context.active_layout().0 as u8);
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;
@@ -427,6 +542,7 @@ impl State {
pub fn ipc_refresh_layout(&mut self) {
self.ipc_refresh_workspaces();
self.ipc_refresh_windows();
self.ipc_refresh_overview();
}
fn ipc_refresh_workspaces(&mut self) {
@@ -459,7 +575,7 @@ impl State {
// 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 != ws.name
|| ipc_ws.name.as_ref() != ws.name()
|| ipc_ws.output.as_ref() != output_name
{
need_workspaces_changed = true;
@@ -474,6 +590,12 @@ impl State {
});
}
// Check if this workspace urgent state changed.
let urgent = ws.is_urgent();
if urgent != ipc_ws.is_urgent {
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
}
// Check if this workspace became focused.
let is_focused = Some(id) == focused_ws_id;
if is_focused && !ipc_ws.is_focused {
@@ -482,7 +604,7 @@ impl State {
}
// Check if this workspace became active.
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx);
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
if is_active && !ipc_ws.is_active {
events.push(Event::WorkspaceActivated { id, focused: false });
}
@@ -503,9 +625,10 @@ impl State {
Workspace {
id,
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name.clone(),
name: ws.name().cloned(),
output: mon.map(|mon| mon.output_name().clone()),
is_active: mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx),
is_urgent: ws.is_urgent(),
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()),
}
@@ -546,28 +669,21 @@ impl State {
}
let Some(ipc_win) = state.windows.get(&id) else {
let window = make_ipc_window(mapped, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
};
let workspace_id = Some(ws_id.get());
let mut changed = ipc_win.workspace_id != workspace_id;
let wl_surface = mapped.toplevel().wl_surface();
changed |= with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
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, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
}
@@ -575,6 +691,11 @@ impl State {
if mapped.is_focused() && !ipc_win.is_focused {
events.push(Event::WindowFocusChanged { id: Some(id) });
}
let urgent = mapped.is_urgent();
if urgent != ipc_win.is_urgent {
events.push(Event::WindowUrgencyChanged { id, urgent })
}
});
// Check for closed windows.
@@ -600,4 +721,22 @@ impl State {
server.send_event(event);
}
}
pub fn ipc_refresh_overview(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.overview;
let is_open = self.niri.layout.is_overview_open();
if state.is_open == is_open {
return;
}
let event = Event::OverviewOpenedOrClosed { is_open };
state.apply(event.clone());
server.send_event(event);
}
}
+218
View File
@@ -0,0 +1,218 @@
use niri_config::layer_rule::LayerRule;
use niri_config::Config;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[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: SolidColorBuffer,
/// The shadow around the surface.
shadow: Shadow,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
/// Scale of the output the layer surface is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
clock: Clock,
}
niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
}
}
impl MappedLayer {
pub fn new(
surface: LayerSurface,
rules: ResolvedLayerRules,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
config: &Config,
) -> Self {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = rules.shadow.resolve_against(shadow_config);
Self {
surface,
rules,
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
view_size,
scale,
shadow: Shadow::new(shadow_config),
clock,
}
}
pub fn update_config(&mut self, config: &Config) {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
self.shadow.update_config(shadow_config);
}
pub fn update_shaders(&mut self) {
self.shadow.update_shaders();
}
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
self.view_size = view_size;
self.scale = scale;
}
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
// Round to physical pixels.
let size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
self.block_out_buffer.resize(size);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
// FIXME: is_active based on keyboard focus?
self.shadow
.update_render_elements(size, true, radius, self.scale, 1.);
}
pub fn are_animations_ongoing(&self) -> bool {
self.rules.baba_is_float
}
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 place_within_backdrop(&self) -> bool {
if !self.rules.place_within_backdrop {
return false;
}
if self.surface.layer() != Layer::Background {
return false;
}
let state = self.surface.cached_state();
if state.exclusive_zone != ExclusiveZone::DontCare {
return false;
}
true
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if !self.rules.baba_is_float {
return Point::from((0., 0.));
}
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
// FIXME: take geometry-corner-radius into account.
let elem = SolidColorRenderElement::from_buffer(
&self.block_out_buffer,
location,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = location;
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;
rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
));
}
rv.normal = render_elements_from_surface_tree(
renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
}
let location = location.to_physical_precise_round(scale).to_logical(scale);
rv.normal
.extend(self.shadow.render(renderer, location).map(Into::into));
rv
}
}
+106
View File
@@ -0,0 +1,106 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
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 layer surface with.
pub opacity: Option<f32>,
/// Whether to block out this layer surface from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
/// Shadow overrides.
pub shadow: ShadowRule,
/// Corner radius to assume this layer surface has.
pub geometry_corner_radius: Option<CornerRadius>,
/// Whether to place this layer surface within the overview backdrop.
pub place_within_backdrop: bool,
/// Whether to bob this window up and down.
pub baba_is_float: bool,
}
impl ResolvedLayerRules {
pub const fn empty() -> Self {
Self {
opacity: None,
block_out_from: None,
shadow: ShadowRule {
off: false,
on: false,
offset: None,
softness: None,
spread: None,
draw_behind_window: None,
color: None,
inactive_color: None,
},
geometry_corner_radius: None,
place_within_backdrop: false,
baba_is_float: false,
}
}
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
let _span = tracy_client::span!("ResolvedLayerRules::compute");
let mut resolved = ResolvedLayerRules::empty();
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);
}
if let Some(x) = rule.geometry_corner_radius {
resolved.geometry_corner_radius = Some(x);
}
if let Some(x) = rule.place_within_backdrop {
resolved.place_within_backdrop = x;
}
if let Some(x) = rule.baba_is_float {
resolved.baba_is_float = x;
}
resolved.shadow.merge_with(&rule.shadow);
}
resolved
}
}
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
if let Some(namespace_re) = &m.namespace {
if !namespace_re.0.is_match(surface.namespace()) {
return false;
}
}
true
}
+3 -5
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
@@ -138,16 +137,15 @@ impl ClosingWindow {
})
}
pub fn advance_animations(&mut self, current_time: Duration) {
pub fn advance_animations(&mut self) {
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let mut anim = anim.restarted(0., 1., 0.);
anim.set_current_time(current_time);
let anim = anim.restarted(0., 1., 0.);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(anim) => anim.set_current_time(current_time),
AnimationState::Animating(_anim) => (),
}
}
File diff suppressed because it is too large Load Diff
+26 -20
View File
@@ -1,8 +1,8 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
use smithay::backend::renderer::element::Kind;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
use smithay::backend::renderer::element::{Element as _, Kind};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::niri_render_elements;
@@ -53,19 +53,24 @@ impl FocusRing {
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
is_urgent: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
let color = if is_urgent {
self.config.urgent_color
} else if is_active {
self.config.active_color
} else {
self.config.inactive_color
@@ -77,7 +82,9 @@ impl FocusRing {
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
let gradient = if is_active {
let gradient = if is_urgent {
self.config.urgent_gradient
} else if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
@@ -86,15 +93,9 @@ impl FocusRing {
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
@@ -178,15 +179,16 @@ impl FocusRing {
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
Rectangle::new(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
} else {
@@ -196,18 +198,16 @@ impl FocusRing {
self.borders[0].update(
self.sizes[0],
Rectangle::from_loc_and_size(
gradient_area.loc - self.locations[0],
gradient_area.size,
),
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
alpha,
);
}
@@ -238,7 +238,9 @@ impl FocusRing {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
let alpha = border.alpha();
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
.into()
};
rv.push(elem);
};
@@ -265,4 +267,8 @@ impl FocusRing {
pub fn is_off(&self) -> bool {
self.config.off
}
pub fn config(&self) -> &niri_config::FocusRing {
&self.config
}
}
+65
View File
@@ -0,0 +1,65 @@
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,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_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,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_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, false, view_rect, radius, scale, 1.);
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
self.inner.render(renderer, location)
}
}
+3241 -2812
View File
File diff suppressed because it is too large Load Diff
+1352 -603
View File
File diff suppressed because it is too large Load Diff
+32 -43
View File
@@ -1,34 +1,31 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
buffer: OffscreenBuffer,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
Shader = ShaderRenderElement,
}
}
@@ -38,12 +35,11 @@ impl OpenAnimation {
Self {
anim,
random_seed: fastrand::f32(),
buffer: OffscreenBuffer::default(),
}
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn advance_animations(&mut self) {}
pub fn is_done(&self) -> bool {
self.anim.is_done()
@@ -58,24 +54,24 @@ impl OpenAnimation {
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
alpha: f32,
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
let (elem, _sync_point, mut data) = self
.buffer
.render(renderer, scale, elements)
.context("error rendering to offscreen buffer")?;
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
// can assume that below.
let offset = elem.offset();
let texture = elem.texture();
let texture_size = elem.logical_size();
let mut area = Rectangle::new(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
let mut target_size = area.size.upscale(1.5);
@@ -102,12 +98,12 @@ impl OpenAnimation {
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return Ok(ShaderRenderElement::new(
let elem = ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
alpha,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
@@ -119,36 +115,29 @@ impl OpenAnimation {
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc)
.into());
.with_location(area.loc);
// We're drawing the shader, not the offscreen itself.
data.id = elem.id().clone();
return Ok((elem.into(), data));
}
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
center.to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
(location + offset).to_physical_precise_round(scale),
location.to_physical_precise_round(scale),
Relocate::Relative,
);
Ok(elem.into())
Ok((elem.into(), data))
}
}
File diff suppressed because it is too large Load Diff
+184
View File
@@ -0,0 +1,184 @@
use std::iter::zip;
use niri_config::CornerRadius;
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
#[derive(Debug)]
pub struct Shadow {
shader_rects: Vec<Rectangle<f64, Logical>>,
shaders: Vec<ShadowRenderElement>,
config: niri_config::Shadow,
}
impl Shadow {
pub fn new(config: niri_config::Shadow) -> Self {
Self {
shader_rects: Vec::new(),
shaders: Vec::new(),
config,
}
}
pub fn update_config(&mut self, config: niri_config::Shadow) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
radius: CornerRadius,
scale: f64,
alpha: f32,
) {
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size is rounded to physical pixels before being passed to this function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
let width = self.config.softness.0;
// Like in CSS box-shadow.
let sigma = width / 2.;
// Adjust width to draw all necessary pixels.
let width = ceil(sigma * 3.);
let offset = self.config.offset;
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
let spread = self.config.spread.0;
let spread = ceil(spread.abs()).copysign(spread);
let offset = offset - Point::from((spread, spread));
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
let box_size = if spread >= 0. {
win_size + Size::from((spread, spread)).upscale(2.)
} else {
// This is a saturating sub.
win_size - Size::from((-spread, -spread)).upscale(2.)
};
let radius = win_radius.expanded_by(spread as f32);
let shader_size = box_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.color
} else {
// Default to slightly more transparent.
self.config
.inactive_color
.unwrap_or(self.config.color * 0.75)
};
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
// This is actually offset relative to shader_geo, this is handled below.
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
if !self.config.draw_behind_window {
let top_left = ceil(f64::from(win_radius.top_left));
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
let bottom_left = f64::min(
win_size.h - top_left,
ceil(f64::from(win_radius.bottom_left)),
);
let bottom_right = f64::min(
win_size.h - top_right,
f64::min(
win_size.w - bottom_left,
ceil(f64::from(win_radius.bottom_right)),
),
);
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
let top_right = Rectangle::new(
Point::from((win_size.w - top_right, 0.)),
Size::from((top_right, top_right)),
);
let bottom_right = Rectangle::new(
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
Size::from((bottom_right, bottom_right)),
);
let bottom_left = Rectangle::new(
Point::from((0., win_size.h - bottom_left)),
Size::from((bottom_left, bottom_left)),
);
let mut background =
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
for rect in &mut background {
rect.loc -= offset;
}
self.shader_rects = shader_geo.subtract_rects(background);
self.shaders
.resize_with(self.shader_rects.len(), Default::default);
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
shader.update(
rect.size,
Rectangle::new(rect.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
win_radius,
alpha,
);
rect.loc += offset;
}
} else {
self.shader_rects.resize_with(1, Default::default);
self.shader_rects[0] = shader_geo;
self.shaders.resize_with(1, Default::default);
self.shaders[0].update(
shader_geo.size,
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
color,
sigma as f32,
radius,
scale as f32,
Rectangle::zero(),
Default::default(),
alpha,
);
self.shader_rects[0].loc += offset;
}
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
if !self.config.on {
return None.into_iter().flatten();
}
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
if !has_shadow_shader {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_rects)
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
Some(rv).into_iter().flatten()
}
}
+412
View File
@@ -0,0 +1,412 @@
use std::iter::zip;
use std::mem;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::tile::Tile;
use super::LayoutElement;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
};
#[derive(Debug)]
pub struct TabIndicator {
shader_locs: Vec<Point<f64, Logical>>,
shaders: Vec<BorderRenderElement>,
open_anim: Option<Animation>,
config: niri_config::TabIndicator,
}
#[derive(Debug)]
pub struct TabInfo {
/// Gradient for the tab indicator.
pub gradient: Gradient,
/// Tab geometry in the same coordinate system as the area.
pub geometry: Rectangle<f64, Logical>,
}
niri_render_elements! {
TabIndicatorRenderElement => {
Gradient = BorderRenderElement,
}
}
impl TabIndicator {
pub fn new(config: niri_config::TabIndicator) -> Self {
Self {
shader_locs: Vec::new(),
shaders: Vec::new(),
open_anim: None,
config,
}
}
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
self.config = config;
}
pub fn update_shaders(&mut self) {
for elem in &mut self.shaders {
elem.damage_all();
}
}
pub fn advance_animations(&mut self) {
if let Some(anim) = &mut self.open_anim {
if anim.is_done() {
self.open_anim = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_anim.is_some()
}
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
}
fn tab_rects(
&self,
area: Rectangle<f64, Logical>,
count: usize,
scale: f64,
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let round = |logical: f64| round_logical_in_physical(scale, logical);
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
let width = round_max1(self.config.width.0);
let gap = self.config.gap.0;
let gap = round_max1(gap.abs()).copysign(gap);
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position;
let side = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
};
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
let min_length = round(side * total_prop.clamp(0., 2.));
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
// growing and shrinking over the duration of the animation.
let pixel = 1. / scale;
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
let length = f64::max(min_length, shortest_length);
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
let px_per_tab = px_per_tab * progress;
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
match position {
TabIndicatorPosition::Left => (),
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
TabIndicatorPosition::Bottom => {
shader_loc.x = shader_loc.y;
shader_loc.y = area.size.h + gap;
}
}
shader_loc += area.loc;
(0..count).map(move |_| {
let mut px_per_tab = px_per_tab;
if ones_left > 0 {
ones_left -= 1;
px_per_tab += pixel;
}
let loc = shader_loc;
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
shader_loc.y += px_per_tab + gaps_between
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
shader_loc.x += px_per_tab + gaps_between
}
}
let size = match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
Size::from((width, px_per_tab))
}
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
Size::from((px_per_tab, width))
}
};
Rectangle::new(loc, size)
})
}
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
enabled: bool,
// Geometry of the tabs area.
area: Rectangle<f64, Logical>,
// View rect relative to the tabs area.
area_view_rect: Rectangle<f64, Logical>,
// Tab count, should match the tabs iterator length.
tab_count: usize,
tabs: impl Iterator<Item = TabInfo>,
is_active: bool,
scale: f64,
) {
if !enabled || self.config.off {
self.shader_locs.clear();
self.shaders.clear();
return;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
self.shader_locs.clear();
self.shaders.clear();
return;
}
self.shaders.resize_with(count, Default::default);
self.shader_locs.resize_with(count, Default::default);
let position = self.config.position;
let radius = self.config.corner_radius.0 as f32;
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
let mut tabs_left = tab_count;
let rects = self.tab_rects(area, count, scale);
for ((shader, loc), (tab, rect)) in zip(
zip(&mut self.shaders, &mut self.shader_locs),
zip(tabs, rects),
) {
*loc = rect.loc;
let mut gradient_area = match tab.gradient.relative_to {
GradientRelativeTo::Window => tab.geometry,
GradientRelativeTo::WorkspaceView => area_view_rect,
};
gradient_area.loc -= *loc;
let mut color_from = tab.gradient.from;
let mut color_to = tab.gradient.to;
if !is_active {
color_from *= 0.5;
color_to *= 0.5;
}
let radius = if shared_rounded_corners && tab_count > 1 {
if tabs_left == tab_count {
// First tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: radius,
top_right: radius,
bottom_right: 0.,
bottom_left: 0.,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: radius,
top_right: 0.,
bottom_right: 0.,
bottom_left: radius,
},
}
} else if tabs_left == 1 {
// Last tab.
match position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
top_left: 0.,
top_right: 0.,
bottom_right: radius,
bottom_left: radius,
},
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
top_left: 0.,
top_right: radius,
bottom_right: radius,
bottom_left: 0.,
},
}
} else {
// Tab in the middle.
CornerRadius::default()
}
} else {
// Separate tabs, or the only tab.
CornerRadius::from(radius)
};
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
tabs_left -= 1;
shader.update(
rect.size,
gradient_area,
tab.gradient.in_,
color_from,
color_to,
((tab.gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_size(rect.size),
0.,
radius,
scale as f32,
1.,
);
}
}
pub fn hit(
&self,
area: Rectangle<f64, Logical>,
tab_count: usize,
scale: f64,
point: Point<f64, Logical>,
) -> Option<usize> {
if self.config.off {
return None;
}
let count = tab_count;
if self.config.hide_when_single_tab && count == 1 {
return None;
}
self.tab_rects(area, count, scale)
.enumerate()
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
pos: Point<f64, Logical>,
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
let has_border_shader = BorderRenderElement::has_shader(renderer);
if !has_border_shader {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_locs)
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
.map(TabIndicatorRenderElement::from);
Some(rv).into_iter().flatten()
}
/// Extra size occupied by the tab indicator.
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
if self.config.off
|| !self.config.place_within_column
|| (self.config.hide_when_single_tab && tab_count == 1)
{
return Size::from((0., 0.));
}
let round = |logical: f64| round_logical_in_physical(scale, logical);
let width = round(self.config.width.0);
let gap = round(self.config.gap.0);
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
// that it peeks from the other side of the window".
let size = f64::max(0., width + gap);
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
}
}
/// Offset of the tabbed content due to space occupied by the tab indicator.
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
match self.config.position {
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
self.extra_size(tab_count, scale).to_point()
}
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
}
}
pub fn config(&self) -> niri_config::TabIndicator {
self.config
}
}
impl TabInfo {
pub fn from_tile<W: LayoutElement>(
tile: &Tile<W>,
position: Point<f64, Logical>,
is_active: bool,
is_urgent: bool,
config: &niri_config::TabIndicator,
) -> Self {
let rules = tile.window().rules();
let rule = rules.tab_indicator;
let gradient_from_rule = || {
let (color, gradient) = if is_urgent {
(rule.urgent_color, rule.urgent_gradient)
} else if is_active {
(rule.active_color, rule.active_gradient)
} else {
(rule.inactive_color, rule.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_config = || {
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
let color = color.map(Gradient::from);
gradient.or(color)
};
let gradient_from_border = || {
// Come up with tab indicator gradient matching the focus ring or the border, whichever
// one is enabled.
let focus_ring_config = tile.focus_ring().config();
let border_config = tile.border().config();
let config = if focus_ring_config.off {
border_config
} else {
focus_ring_config
};
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
};
gradient.unwrap_or_else(|| Gradient::from(color))
};
let gradient = gradient_from_rule()
.or_else(gradient_from_config)
.unwrap_or_else(gradient_from_border);
let geometry = Rectangle::new(position, tile.animated_tile_size());
TabInfo { gradient, geometry }
}
}
+3666
View File
File diff suppressed because it is too large Load Diff
+407 -129
View File
@@ -1,29 +1,32 @@
use core::f64;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::shadow::Shadow;
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
RESIZE_ANIMATION_THRESHOLD,
HitType, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
SizeFrac, RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::Animation;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::render_helpers::RenderTarget;
use crate::utils::transaction::Transaction;
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -35,11 +38,11 @@ pub struct Tile<W: LayoutElement> {
border: FocusRing,
/// The focus ring around the window.
///
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
/// currently here.
focus_ring: FocusRing,
/// The shadow around the window.
shadow: Shadow,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
@@ -49,8 +52,27 @@ pub struct Tile<W: LayoutElement> {
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<f64, Logical>,
/// Whether the tile should float upon unfullscreening.
pub(super) unfullscreen_to_floating: bool,
/// The size that the window should assume when going floating.
///
/// This is generally the last size the window had when it was floating. It can be unknown if
/// the window starts out in the tiling layout or fullscreen.
pub(super) floating_window_size: Option<Size<i32, Logical>>,
/// The position that the tile should assume when going floating, relative to the floating
/// space working area.
///
/// This is generally the last position the tile had when it was floating. It can be unknown if
/// the window starts out in the tiling layout.
pub(super) floating_pos: Option<Point<f64, SizeFrac>>,
/// Currently selected preset width index when this tile is floating.
pub(super) floating_preset_width_idx: Option<usize>,
/// Currently selected preset height index when this tile is floating.
pub(super) floating_preset_height_idx: Option<usize>,
/// The animation upon opening a window.
open_animation: Option<OpenAnimation>,
@@ -64,17 +86,31 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// The animation of the tile's opacity.
pub(super) alpha_animation: Option<AlphaAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
/// Snapshot of the last render for use in the close animation.
unmap_snapshot: Option<TileRenderSnapshot>,
/// Extra damage for clipped surface corner radius changes.
rounded_corner_damage: RoundedCornerDamage,
/// The view size for the tile's workspace.
///
/// Used as the fullscreen target size.
view_size: Size<f64, Logical>,
/// Scale of the output the tile is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
pub options: Rc<Options>,
pub(super) options: Rc<Options>,
}
niri_render_elements! {
@@ -85,12 +121,14 @@ niri_render_elements! {
Opening = OpeningWindowRenderElement,
Resize = ResizeRenderElement,
Border = BorderRenderElement,
Shadow = ShadowRenderElement,
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
}
}
type TileRenderSnapshot =
pub type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
@@ -98,6 +136,7 @@ struct ResizeAnimation {
anim: Animation,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
offscreen: OffscreenBuffer,
}
#[derive(Debug)]
@@ -106,31 +145,74 @@ struct MoveAnimation {
from: f64,
}
#[derive(Debug)]
pub(super) struct AlphaAnimation {
pub(super) anim: Animation,
/// Whether the animation should persist after it's done.
///
/// This is used by things like interactive move which need to animate alpha to
/// semitransparent, then hold it at semitransparent for a while, until the operation
/// completes.
pub(super) hold_after_done: bool,
offscreen: OffscreenBuffer,
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
pub fn new(
window: W,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
options: Rc<Options>,
) -> Self {
let rules = window.rules();
let border_config = rules.border.resolve_against(options.border);
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
let shadow_config = rules.shadow.resolve_against(options.shadow);
let is_fullscreen = window.is_fullscreen();
Self {
window,
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
fullscreen_size: Default::default(),
shadow: Shadow::new(shadow_config),
is_fullscreen,
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
unfullscreen_to_floating: false,
floating_window_size: None,
floating_pos: None,
floating_preset_width_idx: None,
floating_preset_height_idx: None,
open_animation: None,
resize_animation: None,
move_x_animation: None,
move_y_animation: None,
alpha_animation: None,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
view_size,
scale,
clock,
options,
}
}
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
pub fn update_config(
&mut self,
view_size: Size<f64, Logical>,
scale: f64,
options: Rc<Options>,
) {
// If preset widths or heights changed, clear our stored preset index.
if self.options.preset_column_widths != options.preset_column_widths {
self.floating_preset_width_idx = None;
}
if self.options.preset_window_heights != options.preset_window_heights {
self.floating_preset_height_idx = None;
}
self.view_size = view_size;
self.scale = scale;
self.options = options;
@@ -143,21 +225,24 @@ impl<W: LayoutElement> Tile<W> {
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
self.fullscreen_backdrop.resize(view_size);
}
pub fn update_shaders(&mut self) {
self.border.update_shaders();
self.focus_ring.update_shaders();
self.shadow.update_shaders();
}
pub fn update_window(&mut self) {
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0., 0.)) {
self.is_fullscreen = self.window.is_fullscreen();
}
self.is_fullscreen = self.window.is_fullscreen();
if let Some(animate_from) = self.window.take_animation_snapshot() {
let size_from = if let Some(resize) = self.resize_animation.take() {
let (size_from, offscreen) = if let Some(resize) = self.resize_animation.take() {
// Compute like in animated_window_size(), but using the snapshot geometry (since
// the current one is already overwritten).
let mut size = animate_from.size;
@@ -168,19 +253,27 @@ impl<W: LayoutElement> Tile<W> {
size.w = size_from.w + (size.w - size_from.w) * val;
size.h = size_from.h + (size.h - size_from.h) * val;
size
// Also try to reuse the existing offscreen buffer if we have one.
(size, resize.offscreen)
} else {
animate_from.size
(animate_from.size, OffscreenBuffer::default())
};
let change = self.window.size().to_f64().to_point() - size_from.to_point();
let change = f64::max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
let anim = Animation::new(
self.clock.clone(),
0.,
1.,
0.,
self.options.animations.window_resize.anim,
);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
snapshot: animate_from,
offscreen,
});
} else {
self.resize_animation = None;
@@ -195,6 +288,9 @@ impl<W: LayoutElement> Tile<W> {
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
self.shadow.update_config(shadow_config);
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
@@ -204,43 +300,53 @@ impl<W: LayoutElement> Tile<W> {
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self, current_time: Duration) {
pub fn advance_animations(&mut self) {
if let Some(open) = &mut self.open_animation {
open.advance_animations(current_time);
if open.is_done() {
self.open_animation = None;
}
}
if let Some(resize) = &mut self.resize_animation {
resize.anim.set_current_time(current_time);
if resize.anim.is_done() {
self.resize_animation = None;
}
}
if let Some(move_) = &mut self.move_x_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_x_animation = None;
}
}
if let Some(move_) = &mut self.move_y_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_y_animation = None;
}
}
if let Some(alpha) = &mut self.alpha_animation {
if !alpha.hold_after_done && alpha.anim.is_done() {
self.alpha_animation = None;
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.are_transitions_ongoing() || self.window.rules().baba_is_float == Some(true)
}
pub fn are_transitions_ongoing(&self) -> bool {
self.open_animation.is_some()
|| self.resize_animation.is_some()
|| self.move_x_animation.is_some()
|| self.move_y_animation.is_some()
|| self
.alpha_animation
.as_ref()
.is_some_and(|alpha| !alpha.anim.is_done())
}
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
let rules = self.window.rules();
let draw_border_with_background = rules
@@ -260,12 +366,29 @@ impl<W: LayoutElement> Tile<W> {
self.animated_window_size(),
is_active,
!draw_border_with_background,
Rectangle::from_loc_and_size(
self.window.is_urgent(),
Rectangle::new(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
),
radius,
self.scale,
1.,
);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
};
self.shadow.update_render_elements(
self.animated_tile_size(),
is_active,
radius,
self.scale,
1.,
);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@@ -273,21 +396,16 @@ impl<W: LayoutElement> Tile<W> {
} else {
draw_border_with_background
};
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
}
.expanded_by(self.focus_ring.width() as f32);
let radius = radius.expanded_by(self.focus_ring.width() as f32);
self.focus_ring.update_render_elements(
self.animated_tile_size(),
is_active,
!draw_focus_ring_with_background,
self.window.is_urgent(),
view_rect,
radius,
self.scale,
1.,
);
}
@@ -305,11 +423,14 @@ impl<W: LayoutElement> Tile<W> {
offset.y += move_.from * move_.anim.value();
}
offset += self.interactive_move_offset;
offset
}
pub fn start_open_animation(&mut self) {
self.open_animation = Some(OpenAnimation::new(Animation::new(
self.clock.clone(),
0.,
1.,
0.,
@@ -337,7 +458,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_x_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
self.move_x_animation = Some(MoveAnimation {
anim,
@@ -356,7 +477,7 @@ impl<W: LayoutElement> Tile<W> {
let anim = self.move_y_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
self.move_y_animation = Some(MoveAnimation {
anim,
@@ -364,6 +485,44 @@ impl<W: LayoutElement> Tile<W> {
});
}
pub fn stop_move_animations(&mut self) {
self.move_x_animation = None;
self.move_y_animation = None;
}
pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) {
let from = from.clamp(0., 1.);
let to = to.clamp(0., 1.);
let (current, offscreen) = if let Some(alpha) = self.alpha_animation.take() {
(alpha.anim.clamped_value(), alpha.offscreen)
} else {
(from, OffscreenBuffer::default())
};
self.alpha_animation = Some(AlphaAnimation {
anim: Animation::new(self.clock.clone(), current, to, 0., config),
hold_after_done: false,
offscreen,
});
}
pub fn ensure_alpha_animates_to_1(&mut self) {
if let Some(alpha) = &self.alpha_animation {
if alpha.anim.to() != 1. {
// Cancel animation instead of starting a new one because the user likely wants to
// see the tile right away.
self.alpha_animation = None;
}
}
}
pub fn hold_alpha_animation_after_done(&mut self) {
if let Some(alpha) = &mut self.alpha_animation {
alpha.hold_after_done = true;
}
}
pub fn window(&self) -> &W {
&self.window
}
@@ -372,16 +531,12 @@ impl<W: LayoutElement> Tile<W> {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
pub fn is_fullscreen(&self) -> bool {
self.is_fullscreen
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<f64> {
pub fn effective_border_width(&self) -> Option<f64> {
if self.is_fullscreen {
return None;
}
@@ -400,7 +555,7 @@ impl<W: LayoutElement> Tile<W> {
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window_size();
let target_size = self.fullscreen_size;
let target_size = self.view_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
@@ -430,8 +585,27 @@ impl<W: LayoutElement> Tile<W> {
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
let mut size = self.window_expected_or_current_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
@@ -451,7 +625,16 @@ impl<W: LayoutElement> Tile<W> {
size
}
fn animated_window_size(&self) -> Size<f64, Logical> {
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
let size = self.window.expected_size();
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
size
}
pub fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
if let Some(resize) = &self.resize_animation {
@@ -468,14 +651,14 @@ impl<W: LayoutElement> Tile<W> {
size
}
fn animated_tile_size(&self) -> Size<f64, Logical> {
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
return size;
}
@@ -494,16 +677,32 @@ impl<W: LayoutElement> Tile<W> {
loc
}
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
point -= self.window_loc().to_f64();
self.window.is_in_input_region(point)
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_size(self.tile_size());
activation_region.contains(point)
}
pub fn hit(&self, point: Point<f64, Logical>) -> Option<HitType> {
let offset = self.bob_offset();
let point = point - offset;
if self.is_in_input_region(point) {
let win_pos = self.buf_loc() + offset;
Some(HitType::Input { win_pos })
} else if self.is_in_activation_region(point) {
Some(HitType::Activate {
is_tab_indicator: false,
})
} else {
None
}
}
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
@@ -521,7 +720,7 @@ impl<W: LayoutElement> Tile<W> {
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window
.request_size(size.to_i32_floor(), animate, transaction);
.request_size(size.to_i32_floor(), false, animate, transaction);
}
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
@@ -556,16 +755,18 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size.to_i32_round());
pub fn request_fullscreen(&mut self, animate: bool, transaction: Option<Transaction>) {
self.window
.request_size(self.view_size.to_i32_round(), true, animate, transaction);
}
pub fn min_size(&self) -> Size<f64, Logical> {
pub fn min_size_nonfullscreen(&self) -> Size<f64, Logical> {
let mut size = self.window.min_size().to_f64();
if let Some(width) = self.effective_border_width() {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
size.w = f64::max(1., size.w);
size.h = f64::max(1., size.h);
@@ -576,10 +777,13 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn max_size(&self) -> Size<f64, Logical> {
pub fn max_size_nonfullscreen(&self) -> Size<f64, Logical> {
let mut size = self.window.max_size().to_f64();
if let Some(width) = self.effective_border_width() {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
if size.w > 0. {
size.w += width * 2.;
}
@@ -591,6 +795,16 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if self.window.rules().baba_is_float != Some(true) {
return Point::from((0., 0.));
}
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
@@ -602,27 +816,38 @@ impl<W: LayoutElement> Tile<W> {
.unwrap_or_else(|| !self.window.has_ssd())
}
fn render_inner<R: NiriRenderer>(
&self,
fn render_inner<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render_inner");
let alpha = if self.is_fullscreen {
let scale = Scale::from(self.scale);
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
// This is here rather than in render_offset() because render_offset() is currently assumed
// by the code to be temporary. So, for example, interactive move will try to "grab" the
// tile at its current render offset and reset the render offset to zero by cancelling the
// tile move animations. On the other hand, bob_offset() is not resettable, so adding it in
// render_offset() would cause obvious animation glitches.
//
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
let window_loc = self.window_loc();
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
let area = Rectangle::new(window_render_loc, animated_window_size);
let rules = self.window.rules();
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
@@ -636,7 +861,7 @@ impl<W: LayoutElement> Tile<W> {
if let Some(resize) = &self.resize_animation {
resize_popups = Some(
self.window
.render_popups(renderer, window_render_loc, scale, alpha, target)
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
.into_iter()
.map(Into::into),
);
@@ -653,15 +878,11 @@ impl<W: LayoutElement> Tile<W> {
target,
);
let current = render_to_encompassing_texture(
gles_renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&window_elements,
)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
let current = resize
.offscreen
.render(gles_renderer, scale, &window_elements)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
@@ -674,7 +895,13 @@ impl<W: LayoutElement> Tile<W> {
clip_to_geometry
};
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
// The offset and size are computed in physical pixels and converted to
// logical with the same `scale`, so converting them back with rounding
// inside the geometry() call gives us the same physical result back.
let texture_current_geo = elem_current.geometry(scale);
let elem = ResizeRenderElement::new(
area,
scale,
@@ -686,12 +913,15 @@ impl<W: LayoutElement> Tile<W> {
resize.anim.clamped_value().clamp(0., 1.) as f32,
radius,
clip_to_geometry,
alpha,
win_alpha,
);
// FIXME: with split popups, this will use the resize element ID for
// popups, but we want the real IDs.
self.window
.set_offscreen_element_id(Some(elem.id().clone()));
// We're drawing the resize shader, not the offscreen directly.
data.id = elem.id().clone();
// This is not a problem for split popups as the code will look for them by
// original id when it doesn't find them on the offscreen.
self.window.set_offscreen_data(Some(data));
resize_shader = Some(elem.into());
}
}
@@ -703,12 +933,11 @@ impl<W: LayoutElement> Tile<W> {
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc,
alpha,
win_alpha,
Kind::Unspecified,
)
.into(),
);
self.window.set_offscreen_element_id(None);
}
}
@@ -719,9 +948,9 @@ impl<W: LayoutElement> Tile<W> {
if resize_shader.is_none() && resize_fallback.is_none() {
let window = self
.window
.render(renderer, window_render_loc, scale, alpha, target);
.render(renderer, window_render_loc, scale, win_alpha, target);
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
let geo = Rectangle::new(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
@@ -763,15 +992,16 @@ impl<W: LayoutElement> Tile<W> {
if radius != CornerRadius::default() && has_border_shader {
return BorderRenderElement::new(
geo.size,
Rectangle::from_loc_and_size((0., 0.), geo.size),
Rectangle::from_size(geo.size),
GradientInterpolation::default(),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_loc_and_size((0., 0.), geo.size),
Rectangle::from_size(geo.size),
0.,
radius,
scale.x as f32,
1.,
)
.with_location(geo.loc)
.into();
@@ -813,81 +1043,98 @@ impl<W: LayoutElement> Tile<W> {
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
rv.chain(elem.into_iter().flatten())
let rv = rv.chain(elem.into_iter().flatten());
rv.chain(self.shadow.render(renderer, location).map(Into::into))
}
pub fn render<R: NiriRenderer>(
&self,
pub fn render<'a, R: NiriRenderer + 'a>(
&'a self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render");
let scale = Scale::from(self.scale);
let tile_alpha = self
.alpha_animation
.as_ref()
.map_or(1., |alpha| alpha.anim.clamped_value()) as f32;
let mut open_anim_elem = None;
let mut alpha_anim_elem = None;
let mut window_elems = None;
self.window().set_offscreen_data(None);
if let Some(open) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements =
self.render_inner(renderer, Point::from((0., 0.)), scale, focus_ring, target);
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match open.render(renderer, &elements, self.tile_size(), location, scale) {
Ok(elem) => {
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
match open.render(
renderer,
&elements,
self.animated_tile_size(),
location,
scale,
tile_alpha,
) {
Ok((elem, data)) => {
self.window().set_offscreen_data(Some(data));
open_anim_elem = Some(elem.into());
}
Err(err) => {
warn!("error rendering window opening animation: {err:?}");
}
}
} else if let Some(alpha) = &self.alpha_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match alpha.offscreen.render(renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
let offset = elem.offset();
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
self.window().set_offscreen_data(Some(data));
alpha_anim_elem = Some(elem.into());
}
Err(err) => {
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
}
}
}
if open_anim_elem.is_none() {
self.window().set_offscreen_element_id(None);
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
}
open_anim_elem
.into_iter()
.chain(alpha_anim_elem)
.chain(window_elems.into_iter().flatten())
}
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) {
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
self.unmap_snapshot = Some(self.render_snapshot(renderer));
}
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) -> TileRenderSnapshot {
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
);
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let blocked_out_contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Screencast,
);
@@ -905,4 +1152,35 @@ impl<W: LayoutElement> Tile<W> {
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
self.unmap_snapshot.take()
}
pub fn border(&self) -> &FocusRing {
&self.border
}
pub fn focus_ring(&self) -> &FocusRing {
&self.focus_ring
}
pub fn options(&self) -> &Rc<Options> {
&self.options
}
#[cfg(test)]
pub fn view_size(&self) -> Size<f64, Logical> {
self.view_size
}
#[cfg(test)]
pub fn verify_invariants(&self) {
use approx::assert_abs_diff_eq;
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
let scale = self.scale;
let size = self.tile_size();
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
}
}
+1310 -3485
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -11,6 +11,7 @@ pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layer;
pub mod layout;
pub mod niri;
pub mod protocols;
@@ -27,3 +28,6 @@ pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
#[cfg(test)]
mod tests;
+49 -37
View File
@@ -5,13 +5,12 @@ use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, mem};
use clap::Parser;
use clap::{CommandFactory, Parser};
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
@@ -33,6 +32,11 @@ 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() {
@@ -44,6 +48,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_writer(io::stderr)
.with_env_filter(env_filter)
.init();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
@@ -54,19 +66,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let cli = Cli::parse();
if cli.session {
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
// the winit backend to be selected instead.
// DISPLAY, WAYLAND_DISPLAY or WAYLAND_SOCKET from our environment if they are set, since
// they will cause the winit backend to be selected instead.
if env::var_os("DISPLAY").is_some() {
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
@@ -75,6 +80,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
if env::var_os("WAYLAND_SOCKET").is_some() {
warn!("running as a session but WAYLAND_SOCKET is set, removing it");
env::remove_var("WAYLAND_SOCKET");
}
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
@@ -82,9 +91,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("XDG_SESSION_TYPE", "wayland");
}
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
@@ -101,6 +107,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
Sub::Panic => cause_panic(),
Sub::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "niri", &mut io::stdout());
return Ok(());
}
}
}
@@ -151,21 +161,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
let mut config_errored = false;
let mut config = Config::load(&path)
.map_err(|err| {
warn!("{err:?}");
config_errored = true;
})
let config_load_result = Config::load(&path);
let config_errored = config_load_result.is_err();
let mut config = config_load_result
.map_err(|err| warn!("{err:?}"))
.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);
@@ -179,11 +180,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
false,
true,
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
let socket_name = state.niri.socket_name.as_deref().unwrap();
env::set_var("WAYLAND_DISPLAY", socket_name);
info!(
"listening on Wayland socket: {}",
@@ -192,8 +195,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
let socket_path = ipc.socket_path.as_deref().unwrap();
env::set_var(SOCKET_PATH_ENV, socket_path);
info!("IPC listening on: {}", socket_path.to_string_lossy());
}
if cli.session {
@@ -226,12 +230,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up config file watcher.
let _watcher = {
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
// watcher thread.
let process = |path: &Path| {
Config::load(path).map_err(|err| {
warn!("{:?}", err.context("error loading config"));
})
};
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(watch_path.clone(), tx);
let watcher = Watcher::new(watch_path.clone(), process, tx);
event_loop
.handle()
.insert_source(rx, move |event, _, state| match event {
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(config) => state.reload_config(config),
calloop::channel::Event::Closed => (),
})
.unwrap();
@@ -239,10 +251,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Spawn commands from cli and auto-start.
spawn(cli.command);
spawn(cli.command, None);
for elem in spawn_at_startup {
spawn(elem.command);
spawn(elem.command, None);
}
// Show the config error notification right away if needed.
@@ -341,7 +353,7 @@ fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
let system_path = system_config_path();
if let Some(path) = default_config_path() {
if path.exists() {
return (path.clone(), path, true);
return (path.clone(), path, false);
}
if system_path.exists() {
+2024 -658
View File
File diff suppressed because it is too large Load Diff
+10 -26
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;
@@ -96,37 +94,23 @@ pub fn refresh(state: &mut State) {
// 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();
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) => (),

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