Compare commits

...

223 Commits

Author SHA1 Message Date
Ivan Molodetskikh acd33653b3 README: Update screenshot 2024-03-09 14:45:18 +04:00
Ivan Molodetskikh f7c6516da7 README: Expand package listing 2024-03-09 14:29:20 +04:00
Ivan Molodetskikh b220420fba README: mention just "Touchpad gestures"
We've got both directions now.
2024-03-09 08:29:51 +04:00
Ivan Molodetskikh bbeaba16a0 Bump version to 0.1.3 2024-03-09 08:28:48 +04:00
Ivan Molodetskikh 9d7c39b89a Reposition outputs after potentially changing mode
Currently outputs aren't repositioned again after a mode change, which
can cause overlaps.
2024-03-09 08:23:57 +04:00
Ivan Molodetskikh 03fe864d07 Add xdg-foreign 2024-03-08 17:08:58 +04:00
Ivan Molodetskikh e45dbb8ef6 Pass through subpixel layout 2024-03-08 17:06:46 +04:00
Ivan Molodetskikh 5c4b71a5a4 Update Smithay and dependencies 2024-03-08 17:06:35 +04:00
Ivan Molodetskikh 348690afb6 Add wp-viewporter
Doesn't hurt I guess.
2024-03-08 16:52:54 +04:00
sodiboo ca22e70cc4 Implement wlr-screencopy v1 (#243)
* Implement wlr-screencopy

* Finish the implementation

Lots of changes, mainly to fix transform handling. Turns out, grim
expects transformed buffers and untransforms them by itself using info
from wl_output. This means that render helpers needed to learn how to
actually render transformed buffers.

Also, it meant that y_invert is no longer needed.

Next, moved the rendering to the Screencopy frame handler. Turns out,
copy() is more or less expected to return immediately, whereas
copy_with_damage() is expected to wait until the next VBlank. At least
that's the intent I parse reading the protocol.

Finally, brought the version from 3 down to 1, because
copy_with_damage() will need bigger changes. Grim still works, others
not really, mainly because they bind v3 unnecessarily, even if they
don't use the damage request.

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-08 04:10:55 -08:00
Ivan Molodetskikh 1a784e6e66 Remove NOTIFY_FD after reading it 2024-03-06 23:06:39 +04:00
Ivan Molodetskikh 3ee2db71a4 CI: Check dinit feature 2024-03-06 21:01:10 +04:00
Ivan Molodetskikh cedfd4944c Adjust comments 2024-03-06 21:01:00 +04:00
metent 431f070481 Add dinit support (#246)
* Add dinit support

- Add --notify-fd cli flag for ready notifications
- Set dinit activation environment when "dinit" feature flag is enabled

* Make systemd and dinit environment activation additive

* Use NOTIFY_FD env variable instead of --notify-fd cli flag for sending ready notifications

* Format with rustfmt
2024-03-06 08:57:43 -08:00
Ivan Molodetskikh 9cbbffc23c Improve spring comments in default config 2024-03-05 19:06:21 +04:00
Ivan Molodetskikh c6a1398d51 Add fuzzel to generate-rpm requires 2024-03-05 13:40:57 +04:00
Ivan Molodetskikh f9127616b0 Implement rubber banding for the vertical gesture 2024-03-05 13:32:57 +04:00
Ivan Molodetskikh ae89b2e514 Implement spring animations 2024-03-05 13:32:52 +04:00
Ivan Molodetskikh 732f7f6f33 animation: Apply slowdown in realtime 2024-03-05 13:23:06 +04:00
Ivan Molodetskikh 8bebd54c6d tile: Prepare for oscillating animations 2024-03-05 10:33:25 +04:00
Ivan Molodetskikh 1978e5b0b8 monitor: Handle switch idx < 0 and >= len 2024-03-05 08:43:20 +04:00
Ivan Molodetskikh 60b02545f3 Move animation to subfolder 2024-03-04 16:01:57 +04:00
Ivan Molodetskikh 2750b2038b Catch panics from edid-rs
Work around an integer overflow.

See: https://github.com/YaLTeR/niri/issues/239
2024-03-03 19:56:52 +04:00
Ivan Molodetskikh c4145b014a Add proper support for center = always in the horizontal gesture 2024-03-03 12:10:25 +04:00
Ivan Molodetskikh 2e51efd3a3 Remake horizontal gesture to snap with inertia 2024-03-03 09:33:00 +04:00
Ivan Molodetskikh caea05433e Extract WORKSPACE_GESTURE_MOVEMENT constant 2024-03-03 09:25:27 +04:00
Ivan Molodetskikh e4f78c26f0 swipe-tracker: Rename retain_recent to trim_history 2024-03-03 08:04:55 +04:00
Ivan Molodetskikh 1548db56ce Fix vertical gesture constant
400 is for width not height.
2024-03-02 21:18:02 +04:00
Ivan Molodetskikh 5f416abcf9 Change horizontal gesture to focus furthest window 2024-03-02 15:48:54 +04:00
Ivan Molodetskikh 66c1272420 Use unaccelerated delta for vertical gesture
With inertia in place it's ready for this.
2024-03-02 15:41:56 +04:00
Ivan Molodetskikh e0ec6e5b11 Make vertical touchpad swipe inertial
Values and implementation are heavily inspired by AdwSwipeTracker.
2024-03-02 14:33:22 +04:00
Ivan Molodetskikh 93243d7772 Disentangle frame callback sequence from real DRM sequence
It can currently happen that the estimated VBlank timer fires right
before a real VBlank, which can cause some sequence collisions, which
might cause frame callbacks to never be sent. To prevent this, just
track the frame callback sequence fully separately. There isn't really
any harm in this, and if we accidentally increment it more frequently
than necessary then nothing terrible will happen.
2024-03-02 08:20:17 +04:00
sodiboo 24537ec2ba Correctly handle parsing of Binds and DefaultColumnWidth (#234)
* add dev dependencies to flake

* parse only one default-column-width

* require exactly one action per bind, and unique keys for binds

* use proper filename for config errors if possible

* fix duplicate keybinds after invalid action, lose some sanity
2024-03-01 03:50:49 -08:00
Ivan Molodetskikh 88ac16c99a tty: Bump sequence on successful queue_frame()
Before this commit:

- niri queues frame
- successful VBlank happens, sequence is bumped, frame callbacks are
  sent
- niri receives commit, redraws, queues next frame, tries to send frame
  callbacks, but there wasn't a new VBlank yet, so the sequence is old,
  and frame callbacks aren't sent
- frame callbacks are sent only next VBlank
2024-03-01 12:56:55 +04:00
Ivan Molodetskikh 0add457cf0 tty: Avoid zero estimated vblank timer 2024-03-01 08:27:44 +04:00
Ivan Molodetskikh 6e5426ef22 Fix center-column regression
Mistake introduced along with the horizontal gesture.
2024-03-01 08:09:03 +04:00
Ivan Molodetskikh 202406aadf Fix presentation feedback panic with zero presentation time 2024-03-01 07:55:09 +04:00
Ivan Molodetskikh 92d9c7ff4f Add emulate-zero-presentation-time debug flag 2024-03-01 07:54:58 +04:00
Ivan Molodetskikh 28977d1d3f Move workspace gesture into monitor & fix missing workspace cleanup 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh ba10bab010 Implement horizontal touchpad swipe 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh 55038b7c07 Pass prev_idx explicitly to animate_view_offset_to_column() 2024-02-29 08:30:46 +04:00
Ivan Molodetskikh 8018839f5d Extract animate_view_offset_to_column() 2024-02-28 17:23:03 +04:00
Ivan Molodetskikh 077f22edd6 Append _fit to animate_view_offset_to_column() 2024-02-28 17:21:08 +04:00
Ivan Molodetskikh 4f7c3300ef Upgrade dependencies 2024-02-28 13:45:12 +04:00
Ivan Molodetskikh 5628bf7d77 Update Smithay 2024-02-28 13:23:15 +04:00
Christian Meissl 719697179f input: add basic touch support 2024-02-28 13:19:41 +04:00
Christian Meissl 5ac350d51c chore: update smithay 2024-02-28 13:19:41 +04:00
Ivan Molodetskikh 494e98c123 Parse CSS colors in {in,}active-color 2024-02-26 09:14:35 +04:00
Ivan Molodetskikh ec156a8587 Add environment {} config section 2024-02-24 10:08:56 +04:00
Ivan Molodetskikh e278e871c3 Expand ~ in spawn 2024-02-24 09:16:44 +04:00
Ivan Molodetskikh ab9d1aab4e Add open-fullscreen window rule 2024-02-24 08:44:21 +04:00
Ivan Molodetskikh 506dcd99d7 Handle un-/fullscreen after initial configure 2024-02-23 17:47:12 +04:00
Ivan Molodetskikh dfbc024127 Rename surface -> toplevel 2024-02-23 17:40:30 +04:00
Ivan Molodetskikh eb2dce1b53 Fix default width fixed not being honored with borders 2024-02-23 14:40:56 +04:00
Ivan Molodetskikh f5b776a947 Fix unset default width causing a window resize right away 2024-02-23 14:31:35 +04:00
Ivan Molodetskikh 6a587245eb Add open-maximized window rule 2024-02-23 14:24:39 +04:00
Ivan Molodetskikh 2317021a7c Implement explicit unmapped window state tracking 2024-02-23 14:01:32 +04:00
Ivan Molodetskikh af6485cd8c Fix new warnings 2024-02-22 14:04:18 +04:00
Ivan Molodetskikh f32a25eefe Improve shader formatting 2024-02-22 10:21:38 +04:00
Ivan Molodetskikh aefbad0cf7 Simplify gradient border shader 2024-02-22 10:17:06 +04:00
Ivan Molodetskikh b091202d86 visual-tests: Add gradient angle and area tests 2024-02-22 08:54:35 +04:00
Ivan Molodetskikh 48f0f6fb3c Implement gradient borders 2024-02-21 22:15:21 +04:00
Ivan Molodetskikh 340bac0690 Remove unnecessary crop bounds during workspace switch 2024-02-21 21:41:12 +04:00
Ivan Molodetskikh d1b8134337 focus-ring: Store config instead of individual fields 2024-02-21 20:54:24 +04:00
Ivan Molodetskikh 646e3d8995 Accept location in FocusRing
Makes it work more like other elements.
2024-02-21 11:08:48 +04:00
Ivan Molodetskikh d1fe6930a7 Move UI elements into submodule 2024-02-21 10:50:30 +04:00
Ivan Molodetskikh 9e60b344d0 Move watcher to utils 2024-02-21 10:45:03 +04:00
Ivan Molodetskikh 2c01cde9be Move spawn to submodule 2024-02-21 10:42:21 +04:00
Ivan Molodetskikh cb9dc9c0cd Move utils to subfolder 2024-02-21 10:33:09 +04:00
Ivan Molodetskikh 73d2807b4b Fix move_window_to_output losing window instead 2024-02-21 09:39:32 +04:00
Ivan Molodetskikh 7d41f113cb Change non-bug error! to warn!
Be consistent with our usage.
2024-02-21 09:20:34 +04:00
Ivan Molodetskikh 63e5cf8798 Add missing qualified path 2024-02-21 09:12:42 +04:00
Ivan Molodetskikh 9ce19ad7de Use niri_render_elements! for the screenshot UI 2024-02-21 09:12:40 +04:00
Ivan Molodetskikh 751f79dc35 Comment out toggle-debug-tint default bind 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh b8aa0a86e7 Fix debug tint desync for new outputs 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh 82fffdea80 Fix locking with DPMS-inactive monitors
This both enables locking while monitors are powered off (they have no
buffer attached at that point on a TTY, so no sensitive content can
become visible), and fixes the condition below to check even if the
rendering was skipped.
2024-02-21 07:40:50 +04:00
Ivan Molodetskikh 5b3bfd95d9 Upgrade logs about removing env vars to warn!
These are more visible now with the --session flag.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 1a15aa704d ci: Check individual features 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh d58a45a96c Add systemd feature flag for systemd-specific things 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 9f1b4ee299 Set XDG_CURRENT_DESKTOP and XDG_SESSION_TYPE from niri itself 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh f0a5e9c933 Add --session CLI flag instead of detection based on systemd service
Allows running without systemd.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh c4c07841d7 niri.service: Put into session.slice
Now that we're separating spawned processes, put ourselves in the more
important session.slice.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 6ba24e341f utils/spawn: Put processes into systemd scopes
This separates them from the niri scope for the purposes of e.g. the OOM
killer only killing the app and not the compositor.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 13b6c74cc3 utils/spawn: Receive grandchild PID 2024-02-20 12:49:52 +04:00
Ivan Molodetskikh d8fb8d5ef0 Update for Smithay MultiGpu shadow copies 2024-02-18 21:12:07 +04:00
Ivan Molodetskikh 2b5eeb6162 Fix fullscreen handling before initial configure 2024-02-18 10:20:34 +04:00
Ivan Molodetskikh 85be5f746c default-config: Clarify how indexed workspace access works 2024-02-17 21:01:10 +04:00
Ivan Molodetskikh dd7362913e Ignore mouse releases for dismissing overlays 2024-02-17 14:07:51 +04:00
Ivan Molodetskikh 62892d6361 Prevent locking while another lock client is already active
Fixes double swaylock from manual + swayidle.
2024-02-17 07:47:06 +04:00
Ivan Molodetskikh 31c13b6a69 default-config: Document enable-color-transformations-capability debug flag 2024-02-17 07:23:43 +04:00
Ivan Molodetskikh baaac2f3c4 Update Smithay 2024-02-16 22:40:37 +04:00
Ivan Molodetskikh 3fdefae45b Bump version to 0.1.2 2024-02-16 18:00:19 +04:00
Ivan Molodetskikh 6345224e95 default-config: Fix spelling mistakes
Ok I added automatic :set spell for KDL now.
2024-02-16 17:40:18 +04:00
Ivan Molodetskikh b3d2096439 Replace set_modified() with manual impl
MSRV moment
2024-02-16 08:46:58 +04:00
Ivan Molodetskikh 94ded2f6a9 CI: Add a MSRV job 2024-02-16 08:33:19 +04:00
Ivan Molodetskikh fa3bc69f94 Add watcher tests 2024-02-15 10:31:53 +04:00
Viktor Pocedulic 363e1d8764 input: enable configuring of trackpoint devices 2024-02-15 10:27:12 +04:00
Ivan Molodetskikh 8e1d4de0dc tty: Filter out interlaced modes
They don't seem to work. wlroots also filters them:
https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/feb54979c0940655e36119c63e18a9ee72cc03b0/backend/drm/drm.c#L1461
2024-02-14 21:14:01 +04:00
Ivan Molodetskikh 72e3fadb9a default-config: Specify example refresh rate with 3 digits
This is the format you need to use.
2024-02-14 19:55:31 +04:00
Ivan Molodetskikh 78cda2e67f tty: Truncate Edid strings to nul
Otherwise they crash in wayland-rs when converting to CString.
2024-02-14 19:49:34 +04:00
Ivan Molodetskikh 924e21f69b Focus output unconditionally after moving window there
Fixes output not getting focus if there was no window to move.
2024-02-14 09:06:13 +04:00
Ivan Molodetskikh befdebfa03 Add the beginnings of window rules 2024-02-14 08:32:14 +04:00
Ivan Molodetskikh 7960a73e9d config: Fix missing layout {} defaulting to 0 gaps 2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 749ee5d627 Do initial configuration right before sending initial configure
Let the toplevel fill in some details about itself.
2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 952dd48115 Deduplicate call to miette hook 2024-02-13 12:16:58 +04:00
Ivan Molodetskikh cbd066ab68 default-config: Document animation properties 2024-02-12 20:46:29 +04:00
Ivan Molodetskikh bccde351fb Update flake.lock 2024-02-12 09:58:04 +04:00
Kiara Grouwstra beaffb1b97 CI: check nix build works 2024-02-12 09:57:34 +04:00
Shawn Wallace 385454378b Implement DRM leasing
Closes #178
2024-02-12 09:48:54 +04:00
Ivan Molodetskikh 18f06a7acd Fix border getting default values for focus ring 2024-02-12 09:34:54 +04:00
Ivan Molodetskikh 6e23073019 Move default_border() into FocusRing 2024-02-12 09:22:22 +04:00
Ivan Molodetskikh a9fcbf81eb Export NIRI_SOCKET to systemd/dbus environment 2024-02-12 08:56:39 +04:00
Ivan Molodetskikh a99f34cba8 tty: Activate monitors on session resume 2024-02-12 08:45:45 +04:00
Ivan Molodetskikh bd2277fa25 tty: Notify idle activity on session resume 2024-02-12 08:42:34 +04:00
Ivan Molodetskikh 67182129ff Add skip-confirmation flag to the quit action 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh d6b116d229 Add missing space 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh c20a843ab2 Add log message when confirming exit dialog 2024-02-12 07:53:48 +04:00
Kiara Grouwstra 1b752fe08f exclude visual tests from nix, closes #181 2024-02-12 00:01:03 +04:00
Ivan Molodetskikh 89f74aae98 freedesktop-screensaver: Filter out non-interesting messages 2024-02-11 23:05:37 +04:00
Ivan Molodetskikh 5e553c2679 Implement org.freedesktop.ScreenSaver Inhibit
xdg-desktop-portal currently has no way of disabling the Inhibit portal
or ever returning an error to the application from it. Thus Flatpak
Firefox will never fall back to its Wayland backend. To remedy this,
let's actually implement the FDO Inhibit interface that the portal can
use.
2024-02-11 22:26:59 +04:00
Ivan Molodetskikh cabf712821 hotkey-overlay: Deduplicate Spawn actions 2024-02-11 09:27:34 +04:00
Ivan Molodetskikh 0931447ec1 Implement error reporting in IPC 2024-02-11 09:19:37 +04:00
Ivan Molodetskikh a388c25795 Update dependencies 2024-02-10 15:01:34 +04:00
Ivan Molodetskikh 5c4d9824a4 Remove logind-zbus dependency
It isn't updated and we don't really need it anyway.
2024-02-10 14:58:22 +04:00
Ivan Molodetskikh ca4ee5ae25 hotkey-overlay: Only show Spawn binds with Mod/Super 2024-02-10 14:37:38 +04:00
Ivan Molodetskikh 93e16a6582 Implement niri msg action 2024-02-10 09:40:32 +04:00
Ivan Molodetskikh 3486fa5536 Remove unused directories workspace dep 2024-02-10 09:34:35 +04:00
Ivan Molodetskikh c022d74c82 Remove extra `` in comment 2024-02-10 09:19:08 +04:00
Ivan Molodetskikh e68641c0a7 Move CLI types to submodule 2024-02-10 08:40:13 +04:00
Ivan Molodetskikh 2a892ef511 input: Fix Clippy warning 2024-02-10 08:38:19 +04:00
Ivan Molodetskikh 90c6721e97 config: Add missing Smithay feature
Fixes build on nightly.
2024-02-10 07:51:53 +04:00
Ivan Molodetskikh e5cd9e9307 default-config: Replace Mod with Super in swaylock bind
Otherwise it conflicts with Mod+L in nested.
2024-02-09 16:23:33 +04:00
Ivan Molodetskikh 573dca10cc input: Fix handling of binds with compositor mod but no explicit Mod 2024-02-09 16:23:05 +04:00
Ivan Molodetskikh 577fba82e5 input: Split bound_action() and add tests 2024-02-09 16:16:18 +04:00
Ivan Molodetskikh b9116c579a Implement idle-notify and idle-inhibit 2024-02-09 15:50:40 +04:00
Ivan Molodetskikh d8dcadc5b2 Clamp animation slowdown to sane values 2024-02-07 20:03:23 +04:00
Ivan Molodetskikh 6424a2738d Make all animations configurable 2024-02-07 17:14:24 +04:00
Ivan Molodetskikh 753a90430a animation: Accept ms as u32
Less boilerplate elsewhere.
2024-02-07 16:32:38 +04:00
Ivan Molodetskikh f9085db564 Implement window open animations 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 49ce791d13 Add a Tracy span to OffscreenRenderElement::new 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 4b8e04da04 Activate the new right_of window on its workspace
This way when a dialog opens on a different workspace, the user will see
it right away when they switch to that workspace.
2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 026ad8f377 Add a way to override the element ID for primary output check 2024-02-07 11:30:52 +04:00
Ivan Molodetskikh 0761401650 Add OffscreenRenderElement 2024-02-07 11:30:33 +04:00
Ivan Molodetskikh 3360517f62 Clear before rendering to texture
Otherwise I see artifacts on some GTK dialogs.
2024-02-07 11:18:55 +04:00
Ivan Molodetskikh 9896fd67a0 Open dialogs to the right of their parent, don't steal focus 2024-02-07 10:49:01 +04:00
Ivan Molodetskikh 15ec699fbb visual-tests: Remove "Just" prefix 2024-02-07 09:24:41 +04:00
Ivan Molodetskikh a1cc39a437 visual-tests/tile: Disable focus ring 2024-02-07 09:22:00 +04:00
Ivan Molodetskikh 738d9a2b40 Add blank line 2024-02-06 19:53:31 +04:00
Ivan Molodetskikh 68752db51b layout: Add Column::advance_animations() 2024-02-06 19:52:47 +04:00
Ivan Molodetskikh d4929b8e18 Inline variable 2024-02-06 19:52:10 +04:00
Ivan Molodetskikh 93c547f749 Move focus ring into Tile
For now, will make the open animation better.
2024-02-06 19:49:51 +04:00
Ivan Molodetskikh e2b91c0c1c layout: Fix refresh in tests
Didn't affect anything but still.
2024-02-06 19:09:27 +04:00
Ivan Molodetskikh 322b5cbac7 Add Layout::with_options() 2024-02-06 19:09:15 +04:00
Ivan Molodetskikh 592791611a Change render functions to accept iterators 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh d073d2ab3d Move render functions to render_helpers 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh b2298db5c5 Split render_helpers.rs 2024-02-06 11:25:25 +04:00
Ivan Molodetskikh baa6263cbe Bump libinput to 1.21, add dwtp flag 2024-02-06 09:54:46 +04:00
Ivan Molodetskikh 795da53d53 README: Update Ubuntu dependencies 2024-02-06 09:49:53 +04:00
Ivan Molodetskikh 122afff7d1 Add niri-visual-tests 2024-02-06 09:40:45 +04:00
Ivan Molodetskikh d2a4e6a0cb Update dependencies 2024-02-06 09:40:34 +04:00
Ivan Molodetskikh 8916b18c6b Run Ubuntu CI in a 23.10 container
We will soon need newer dependencies.
2024-02-06 09:40:32 +04:00
Ivan Molodetskikh b0d0fce5f3 Move use into feature-gated function 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3dc4a5fdac Fix Clippy warnings 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 1706a46b2b layout: Mark some things as pub 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3789d85588 Add lib.rs, become a mixed lib-bin crate
Will be used for visual tests.
2024-02-05 17:40:16 +04:00
Dennis Ranke 3a23417e98 Add consume-or-expel-window-left/right commands 2024-02-05 14:09:47 +04:00
Ivan Molodetskikh 6bb83757ee Convert everything to niri_render_elements! {} 2024-02-05 14:05:08 +04:00
Ivan Molodetskikh b62a07956a Add niri_render_elements! {}
We will be using this in several other places.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 96016790b2 layout: Replace with_tiles_in_render_order() with Iterator 2024-02-05 13:55:09 +04:00
Ivan Molodetskikh bf978fe98d layout/tile: Return Iterator of render elements
Avoid a Vec.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 57521c69c3 layout: Add TileRenderElement 2024-02-04 22:52:11 +04:00
Ivan Molodetskikh da826e42aa layout: Add LayoutElementRenderElement
Allows for testing layout rendering without Wayland windows.
2024-02-04 22:31:44 +04:00
Ivan Molodetskikh b824cf90ab layout: Generalize traversal between rendering and input 2024-02-04 22:10:26 +04:00
Ivan Molodetskikh 7a4bb8ba8a layout: Make rendering not Window-specific
Doesn't need to be any more.
2024-02-04 21:23:00 +04:00
Ivan Molodetskikh 72c8f569ac Bump version to 0.1.1 2024-02-03 10:00:06 +04:00
Ivan Molodetskikh 798d9c55df Support fullscreen for new windows 2024-02-03 09:45:26 +04:00
Ivan Molodetskikh 05613eed1e Verify that pending fullscreen matches column 2024-02-03 09:44:34 +04:00
Ivan Molodetskikh b23dd4b800 Respect natural-scroll for workspace switch gesture 2024-02-03 09:00:08 +04:00
Ivan Molodetskikh 1f72089a46 Place new workspace after current when moving
This feels more natural, also makes moving back and forth idempotent in
most cases.
2024-02-03 08:42:56 +04:00
Ivan Molodetskikh fbe9020915 Update dependencies 2024-02-02 17:04:17 +04:00
Ivan Molodetskikh 2036116f16 config: Premultiply alpha in Color when converting to f32
Smithay wants premultiplied alpha.
2024-02-01 18:53:45 +04:00
Ivan Molodetskikh 9afd728ae9 Add error messages to backend initialization 2024-02-01 16:55:46 +04:00
Andreas Stührk e51268a39e Add actions to move the active workspace to another monitor 2024-02-01 12:29:46 +04:00
Ivan Molodetskikh 0a715ce155 default-config: Improve wording for focus-ring/border comment
SSD or server-side decorations is never mentioned elsewhere.
2024-02-01 12:06:13 +04:00
Ivan Molodetskikh 89ac958670 default-config: Document how focus ring and border draw behind
Related: https://github.com/YaLTeR/niri/issues/150
2024-02-01 10:08:15 +04:00
Ivan Molodetskikh 2e50f8dee0 Hardcode winit transform for now 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 7052f0129e Stop screencasts on size changes 2024-01-31 23:02:38 +04:00
axtloss 962e159db6 Add option to rotate outputs 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 11bff3a2f1 Update Smithay (rotation fix) 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 15606304f2 README: Bring AUR link back 2024-01-30 22:36:30 -08:00
Christian Meissl 85eac9d9d0 chore: bump smithay
includes fixes for wrong direct scan-out transform
and damage artifacts on output transform changes.
also includes a fix for a race in popup surface re-use.
2024-01-30 15:30:31 +04:00
Ivan Molodetskikh d3f4583c90 foreign_toplevel: Use OutputHandler to send output_enter on demand 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh fefb1cccd6 foreign_toplevel: Update the focused window last 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh deef52519a foreign_toplevel: Change activated to mean keyboard focus 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh 59ff331597 Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
2024-01-30 12:30:57 +04:00
Christian Meissl b813f99abd tty: reset surface state after changing monitor state
changing the "ACTIVE" property of a surface requires
to re-evaluate the surface state.
2024-01-30 08:03:21 +04:00
Ivan Molodetskikh d9b9cec8b8 README: Remove AUR link for now
It doesn't work properly yet apparently.
2024-01-29 12:29:32 -08:00
Christian Meissl 597ea62d17 input: update keyboard led state 2024-01-28 23:43:08 +04:00
Ivan Molodetskikh 51243a0a50 Show notification about creating a default config 2024-01-28 17:15:47 +04:00
Ivan Molodetskikh 0ebcc3e0d6 Create default config file if missing 2024-01-28 17:15:33 +04:00
Ivan Molodetskikh 64c85d865e winit: Don't remove output on CloseRequested
More winit events can process after CloseRequested, which will cause a
panic if trying to access the now-removed output.
2024-01-28 16:30:29 +04:00
Ivan Molodetskikh 367e4955ea Mark Msg as pub
Seems to break the build on 1.72.0 otherwise.
2024-01-28 09:34:42 +04:00
Ivan Molodetskikh dd967554d1 Bump version to 0.1.0 2024-01-27 14:10:31 +04:00
Ivan Molodetskikh 6d7c220137 Try harder to find an output for the screenshot UI
The mouse might be outside any outputs, let's try to open in that case
anyway.
2024-01-27 14:09:55 +04:00
Ivan Molodetskikh d77aac1afa Fix damage when rendering to texture 2024-01-27 10:50:40 +04:00
Ivan Molodetskikh 837a0a20fb Update README 2024-01-25 08:34:42 +04:00
Ivan Molodetskikh ecdf756b55 Name output render element better 2024-01-25 08:02:33 +04:00
Christian Meissl 73f3c160b2 use pixman for cursor plane rendering 2024-01-25 07:49:51 +04:00
Christian Meissl 5f99eb13ab Remove hack for fixed EGLDisplay issue 2024-01-25 07:49:51 +04:00
Christian Meissl 20326b093c Update smithay 2024-01-25 07:49:51 +04:00
Ivan Molodetskikh 467d92a4b4 github: Add a feature request link to start a discussion 2024-01-23 17:41:35 +04:00
Ivan Molodetskikh 15bb69c0b9 Update issue templates 2024-01-23 05:36:19 -08:00
Ivan Molodetskikh adfbfdffb3 Create a bug report template 2024-01-23 05:34:38 -08:00
Ivan Molodetskikh 087ed260c5 Update Smithay (find_popup_root_surface() panic fix) 2024-01-23 17:12:47 +04:00
Ivan Molodetskikh f5642ab733 Ignore popup grabs when IME keyboard grab is active
Doing this properly will require more refactors, potentially in Smithay.
For now let's just ignore popup grabs to make popups work.
2024-01-23 17:05:08 +04:00
Ivan Molodetskikh ab9706cb30 screencast: Emit MonitorsChanged 2024-01-23 12:02:52 +04:00
Ivan Molodetskikh 05f2a3709b srceencast: Send stream size
Kooha requires this (even though it's optional). Unfortunately, Kooha
also seems to want memfd recording so it doesn't work anyway.
2024-01-23 11:36:11 +04:00
Ivan Molodetskikh 743173ef64 config: Bump precision on the default widths
This seems to actually matter on my 2560x display.
2024-01-22 20:43:33 +04:00
Ivan Molodetskikh cbbb7a26fc Update Smithay, use device changed session resume code
Should fix most cases of monitors failing to light up after a TTY
switch.
2024-01-22 16:13:39 +04:00
sodiboo 18566e3366 Watch for canonical filename, not just mtime 2024-01-22 07:42:45 +04:00
Ivan Molodetskikh df48337d83 tty: Delay output config update until resume
We can't do anything while paused.
2024-01-21 10:25:39 +04:00
Ivan Molodetskikh f5e9b40140 tty: Check changes against pending connectors and mode
If we queued some DRM changes, they will be in pending. Also be more
resilient by removing unwrap.
2024-01-21 10:24:42 +04:00
Ivan Molodetskikh 5cacd03e85 Return error instead of broken screenshot for portal 2024-01-21 10:03:13 +04:00
79 changed files with 11292 additions and 2510 deletions
+21
View File
@@ -0,0 +1,21 @@
---
name: Bug report
about: Report a bug or a crash
title: ''
labels: bug
assignees: ''
---
<!-- Please describe the issue here at the top, then fill in the system information below. -->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
* CPU:
+4
View File
@@ -0,0 +1,4 @@
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)
+94 -24
View File
@@ -24,6 +24,7 @@ jobs:
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -32,34 +33,90 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y 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
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
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.configuration }}
- name: Build (no default features)
run: cargo build ${{ matrix.release-flag }} --no-default-features
- name: Check (no default features)
run: cargo check ${{ matrix.release-flag }} --no-default-features
- name: Build
run: cargo build ${{ matrix.release-flag }}
- name: Check (just dbus)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
- name: Check (just systemd)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
- name: Check (just dinit)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
- name: Check (just xdp-gnome-screencast)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
- name: Check
run: cargo check ${{ matrix.release-flag }}
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build Tests
run: cargo test --no-run --all ${{ matrix.release-flag }}
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
visual-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- 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
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --package niri-visual-tests
msrv:
strategy:
fail-fast: false
name: 'msrv - 1.72.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- 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
- uses: dtolnay/rust-toolchain@1.72.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-targets
clippy:
strategy:
@@ -67,6 +124,7 @@ jobs:
name: clippy
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -75,15 +133,12 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y 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
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
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal --component clippy
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
@@ -119,8 +174,23 @@ 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
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
- uses: Swatinem/rust-cache@v2
- run: cargo build
- run: cargo build --all
nix:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Check flake inputs
uses: DeterminateSystems/flake-checker-action@v4
continue-on-error: true
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- run: nix build
continue-on-error: true
Generated
+763 -338
View File
File diff suppressed because it is too large Load Diff
+37 -24
View File
@@ -1,5 +1,8 @@
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.0-beta.1"
version = "0.1.3"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -7,11 +10,13 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
anyhow = "1.0.80"
bitflags = "2.4.2"
directories = "5.0.1"
serde = { version = "1.0.195", features = ["derive"] }
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.5", default-features = false }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.0", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -35,38 +40,39 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.79" }
anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.1.1", optional = true }
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.2"
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
clap = { version = "4.4.18", features = ["derive", "string"] }
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.25.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.152"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
logind-zbus = { version = "3.1.2", optional = true }
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
libc = "0.2.153"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.3", path = "niri-config" }
niri-ipc = { version = "0.1.3", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.18.0"
pipewire = { version = "0.7.2", optional = true }
png = "0.17.11"
pangocairo = "0.19.2"
pipewire = { version = "0.8.0", optional = true }
png = "0.17.13"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.13"
profiling = "1.0.15"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.111"
serde_json = "1.0.114"
smithay-drm-extras.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -80,6 +86,7 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
@@ -88,15 +95,20 @@ features = [
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
xshell = "0.2.5"
[features]
default = ["dbus", "xdp-gnome-screencast"]
# Enables DBus support (required for xdp-gnome and power button inhibiting).
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables dinit integration (global environment).
dinit = []
[profile.release]
debug = "line-tables-only"
@@ -108,7 +120,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~beta.1"
version = "0.1.3"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -119,3 +131,4 @@ assets = [
]
[package.metadata.generate-rpm.requires]
alacritty = "*"
fuzzel = "*"
+29 -19
View File
@@ -6,7 +6,7 @@
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/16f87a4a-afac-49aa-b3e6-5e6f16c943a9)
![](https://github.com/YaLTeR/niri/assets/1794388/2b246c2c-7cf3-4a11-96eb-ad0c7f2f4ed6)
## About
@@ -16,26 +16,33 @@ Opening a new window never causes existing windows to resize.
Every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
Since windows go left-to-right horizontally, workspaces are arranged vertically.
Workspaces are dynamic and arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Features
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- Touchpad gesture to switch workspaces
- Touchpad gestures
- Configurable layout: gaps, borders, struts, window sizes
- Live-reloading config
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
Note that NVIDIA GPUs might have rendering issues.
## Inspiration
@@ -44,25 +51,25 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Packages
There are several community-maintained distribution packages that you can use to install niri.
Here are some of them:
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
- NixOS Flake: https://github.com/sodiboo/niri-flake
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
## Building
> [!TIP]
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> For NixOS users, check out https://github.com/sodiboo/niri-flake
First, install the dependencies for your distribution.
- Ubuntu:
- Ubuntu 23.10:
```sh
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
```
- Fedora:
@@ -71,7 +78,9 @@ First, install the dependencies for your distribution.
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
```
Next, build niri with `cargo build --release`.
Next, get latest stable Rust: https://rustup.rs/
Then, build niri with `cargo build --release`.
### NixOS/Nix
@@ -184,7 +193,6 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
## Configuration
@@ -202,4 +210,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[PaperWM]: https://github.com/paperwm/PaperWM
[mako]: https://github.com/emersion/mako
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
Generated
+18 -18
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1702918879,
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
@@ -28,11 +28,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1701411808,
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
"lastModified": 1709274179,
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@@ -62,11 +62,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1701697642,
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"type": "github"
},
"original": {
@@ -77,11 +77,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1702900294,
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"type": "github"
},
"original": {
@@ -103,11 +103,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1701372675,
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
"lastModified": 1709219524,
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
"type": "github"
},
"original": {
+10 -10
View File
@@ -39,22 +39,22 @@
pname = "niri";
version = self.rev or "dirty";
src = nix-filter.lib.filter {
root = ./.;
include = [
./src
./niri-config
./niri-ipc
./Cargo.toml
./Cargo.lock
./resources
];
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));
};
nativeBuildInputs = with pkgs; [
pkg-config
autoPatchelfHook
clang
gdk-pixbuf
graphene
gtk4
libadwaita
];
buildInputs = with pkgs; [
+4 -1
View File
@@ -9,8 +9,11 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.6.2"
knuffel = "3.2.0"
miette = "5.10.0"
smithay.workspace = true
niri-ipc = { version = "0.1.3", path = "../niri-ipc" }
regex = "1.10.3"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+1078 -138
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -8,4 +8,8 @@ edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
serde.workspace = true
[features]
clap = ["dep:clap"]
+260 -3
View File
@@ -2,6 +2,7 @@
#![warn(missing_docs)]
use std::collections::HashMap;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
@@ -9,21 +10,225 @@ use serde::{Deserialize, Serialize};
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Request {
/// Request information about connected outputs.
Outputs,
/// Perform an action.
Action(Action),
}
/// Response from niri to client.
#[derive(Debug, Serialize, Deserialize)]
/// Reply from niri to client.
///
/// Every request gets one reply.
///
/// * If an error had occurred, it will be an `Reply::Err`.
/// * If the request does not need any particular response, it will be
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
pub type Reply = Result<Response, String>;
/// Successful response from niri to client.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Response {
/// A request that does not need a response was handled successfully.
Handled,
/// Information about connected outputs.
///
/// Map from connector name to output info.
Outputs(HashMap<String, Output>),
}
/// Actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
// variants from niri-config should be present here.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
pub enum Action {
/// Exit niri.
Quit {
/// Skip the "Press Enter to confirm" prompt.
#[cfg_attr(feature = "clap", arg(short, long))]
skip_confirmation: bool,
},
/// Power off all monitors via DPMS.
PowerOffMonitors,
/// Spawn a command.
Spawn {
/// Command to spawn.
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: Vec<String>,
},
/// Open the screenshot UI.
Screenshot,
/// Screenshot the focused screen.
ScreenshotScreen,
/// Screenshot the focused window.
ScreenshotWindow,
/// Close the focused window.
CloseWindow,
/// Toggle fullscreen on the focused window.
FullscreenWindow,
/// Focus the column to the left.
FocusColumnLeft,
/// Focus the column to the right.
FocusColumnRight,
/// Focus the first column.
FocusColumnFirst,
/// Focus the last column.
FocusColumnLast,
/// Focus the window below.
FocusWindowDown,
/// Focus the window above.
FocusWindowUp,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceDown,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp,
/// Move the focused column to the left.
MoveColumnLeft,
/// Move the focused column to the right.
MoveColumnRight,
/// Move the focused column to the start of the workspace.
MoveColumnToFirst,
/// Move the focused column to the end of the workspace.
MoveColumnToLast,
/// Move the focused window down in a column.
MoveWindowDown,
/// Move the focused window up in a column.
MoveWindowUp,
/// Move the focused window down in a column or to the workspace below.
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 the window to the right into the focused column.
ConsumeWindowIntoColumn,
/// Expel the focused window from the column.
ExpelWindowFromColumn,
/// Center the focused column on the screen.
CenterColumn,
/// Focus the workspace below.
FocusWorkspaceDown,
/// Focus the workspace above.
FocusWorkspaceUp,
/// Focus a workspace by index.
FocusWorkspace {
/// Index of the workspace to focus.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by index.
MoveWindowToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
/// Move the focused column to a workspace by index.
MoveColumnToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
/// Move the focused workspace up.
MoveWorkspaceUp,
/// Focus the monitor to the left.
FocusMonitorLeft,
/// Focus the monitor to the right.
FocusMonitorRight,
/// Focus the monitor below.
FocusMonitorDown,
/// Focus the monitor above.
FocusMonitorUp,
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft,
/// Move the focused window to the monitor to the right.
MoveWindowToMonitorRight,
/// Move the focused window to the monitor below.
MoveWindowToMonitorDown,
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp,
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft,
/// Move the focused column to the monitor to the right.
MoveColumnToMonitorRight,
/// Move the focused column to the monitor below.
MoveColumnToMonitorDown,
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp,
/// Change the height of the focused window.
SetWindowHeight {
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between preset column widths.
SwitchPresetColumnWidth,
/// Toggle the maximized state of the focused column.
MaximizeColumn,
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
#[cfg_attr(feature = "clap", arg())]
layout: LayoutSwitchTarget,
},
/// Show the hotkey overlay.
ShowHotkeyOverlay,
/// Move the focused workspace to the monitor to the left.
MoveWorkspaceToMonitorLeft,
/// Move the focused workspace to the monitor to the right.
MoveWorkspaceToMonitorRight,
/// Move the focused workspace to the monitor below.
MoveWorkspaceToMonitorDown,
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp,
/// Toggle a debug tint on windows.
ToggleDebugTint,
}
/// Change in window or column size.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
/// Set the size in logical pixels.
SetFixed(i32),
/// Set the size as a proportion of the working area.
SetProportion(f64),
/// Add or subtract to the current size in logical pixels.
AdjustFixed(i32),
/// Add or subtract to the current size as a proportion of the working area.
AdjustProportion(f64),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutSwitchTarget {
/// The next configured layout.
Next,
/// The previous configured layout.
Prev,
}
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
@@ -53,3 +258,55 @@ pub struct Mode {
/// Refresh rate in millihertz.
pub refresh_rate: u32,
}
impl FromStr for SizeChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err("trailing characters after '%' are not allowed");
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err("value is missing"),
}
}
None => {
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;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
}
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "niri-visual-tests"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.3", path = ".." }
niri-config = { version = "0.1.3", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+14
View File
@@ -0,0 +1,14 @@
# niri-visual-tests
> [!NOTE]
>
> This is a development-only app, you shouldn't package it.
This app contains a number of hard-coded test scenarios for visual inspection.
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
The idea is to go through the test scenarios and check that everything *looks* right.
## Running
You will need recent GTK and libadwaita.
Then, `cargo run`.
+3
View File
@@ -0,0 +1,3 @@
.anim-control-bar {
padding: 12px;
}
@@ -0,0 +1,76 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::gradient::GradientRenderElement;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientAngle {
angle: f32,
prev_time: Duration,
}
impl GradientAngle {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientAngle {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut 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. {
self.angle -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size);
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
self.angle - FRAC_PI_2,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,116 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::gradient::GradientRenderElement;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientArea {
progress: f32,
border: FocusRing,
prev_time: Duration,
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let mut border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
active_color: Color::new(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
});
border.set_active(true);
Self {
progress: 0.,
border,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientArea {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut 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. {
self.progress -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let mut rv = Vec::new();
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size);
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
));
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
self.border.update(g_size, true);
rv.extend(
self.border
.render(
renderer,
Point::from(g_loc),
Scale::from(1.),
size.to_logical(1),
)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
g_area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
FRAC_PI_4,
)
.into_iter()
.map(|elem| Box::new(elem) as _),
);
rv
}
}
+230
View File
@@ -0,0 +1,230 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
use niri::layout::Options;
use niri::utils::get_monotonic_time;
use niri_config::Color;
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 super::TestCase;
use crate::test_window::TestWindow;
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
pub struct Layout {
output: Output,
windows: Vec<TestWindow>,
layout: niri::layout::Layout<TestWindow>,
start_time: Duration,
steps: HashMap<Duration, DynStepFn>,
}
impl Layout {
pub fn new(size: Size<i32, Logical>) -> Self {
let output = Output::new(
String::new(),
PhysicalProperties {
size: Size::from((size.w, size.h)),
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
},
);
let mode = Some(Mode {
size: size.to_physical(1),
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
active_gradient: None,
inactive_gradient: None,
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(options);
layout.add_output(output.clone());
Self {
output,
windows: Vec::new(),
layout,
start_time: get_monotonic_time(),
steps: HashMap::new(),
}
}
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.layout.activate_window(&rv.windows[0]);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
});
rv
}
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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.layout.start_open_animation_for_window(&win);
});
}
rv
}
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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.layout.start_open_animation_for_window(&win);
});
}
rv
}
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::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.layout.start_open_animation_for_window(&win);
});
rv
}
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::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.layout.start_open_animation_for_window(&win);
});
rv
}
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
self.layout.add_window(window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
}
self.windows.push(window);
}
fn add_window_right_of(
&mut self,
right_of: &TestWindow,
window: TestWindow,
width: Option<ColumnWidth>,
) {
self.layout
.add_window_right_of(right_of, window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
}
self.windows.push(window);
}
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
self.steps
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
}
}
impl TestCase for Layout {
fn resize(&mut self, width: i32, height: i32) {
let mode = Some(Mode {
size: Size::from((width, height)),
refresh: 60000,
});
self.output.change_current_state(mode, None, None, None);
layer_map_for_output(&self.output).arrange();
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win);
}
}
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
let run = self
.steps
.keys()
.copied()
.filter(|delay| self.start_time + *delay <= current_time)
.collect::<Vec<_>>();
for key in &run {
let f = self.steps.remove(key).unwrap();
f(self);
}
if !run.is_empty() {
current_time = get_monotonic_time();
}
self.layout.advance_animations(current_time);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::time::Duration;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
pub mod layout;
pub mod tile;
pub mod window;
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
false
}
fn advance_animations(&mut self, _current_time: Duration) {}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
}
+117
View File
@@ -0,0 +1,117 @@
use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Tile {
window: TestWindow,
tile: niri::layout::tile::Tile<TestWindow>,
}
impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::freeform(size);
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);
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);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn with_window(window: TestWindow) -> Self {
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 32,
active_color: Color::new(255, 163, 72, 255),
..Default::default()
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
Self { window, tile }
}
}
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
self.tile.request_tile_size(Size::from((width, height)));
self.window.communicate();
}
fn are_animations_ongoing(&self) -> bool {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time, true);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
self.tile
.render(
renderer,
location,
Scale::from(1.),
size.to_logical(1),
true,
)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+57
View File
@@ -0,0 +1,57 @@
use niri::layout::LayoutElement;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Window {
window: TestWindow,
}
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
window.request_size(size);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.request_size(size);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(size);
window.communicate();
Self { window }
}
}
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)));
self.window.communicate();
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let win_size = self.window.size().to_physical(1);
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
self.window
.render(renderer, location, Scale::from(1.))
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+170
View File
@@ -0,0 +1,170 @@
#[macro_use]
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::tile::Tile;
use cases::window::Window;
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use gtk::{gdk, gio, glib};
use niri::animation::ANIMATION_SLOWDOWN;
use smithay::utils::{Logical, Size};
use smithay_view::SmithayView;
use tracing_subscriber::EnvFilter;
use crate::cases::gradient_angle::GradientAngle;
use crate::cases::gradient_area::GradientArea;
use crate::cases::layout::Layout;
use crate::cases::TestCase;
mod cases;
mod smithay_view;
mod test_window;
fn main() -> glib::ExitCode {
let directives =
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
app.connect_startup(on_startup);
app.connect_activate(build_ui);
app.run()
}
fn on_startup(_app: &adw::Application) {
// Load our CSS.
let provider = gtk::CssProvider::new();
provider.load_from_string(include_str!("../resources/style.css"));
if let Some(display) = gdk::Display::default() {
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
struct S {
stack: gtk::Stack,
}
impl S {
fn add<T: TestCase + 'static>(
&self,
make: impl Fn(Size<i32, Logical>) -> T + 'static,
title: &str,
) {
let view = SmithayView::new(make);
self.stack.add_titled(&view, None, title);
}
}
let s = S {
stack: stack.clone(),
};
s.add(Window::freeform, "Freeform Window");
s.add(Window::fixed_size, "Fixed Size Window");
s.add(
Window::fixed_size_with_csd_shadow,
"Fixed Size Window - CSD Shadow",
);
s.add(Tile::freeform, "Freeform Tile");
s.add(Tile::fixed_size, "Fixed Size Tile");
s.add(
Tile::fixed_size_with_csd_shadow,
"Fixed Size Tile - CSD Shadow",
);
s.add(Tile::freeform_open, "Freeform Tile - Open");
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
s.add(
Tile::fixed_size_with_csd_shadow_open,
"Fixed Size Tile - CSD Shadow - Open",
);
s.add(Layout::open_in_between, "Layout - Open In-Between");
s.add(
Layout::open_multiple_quickly,
"Layout - Open Multiple Quickly",
);
s.add(
Layout::open_multiple_quickly_big,
"Layout - Open Multiple Quickly - Big",
);
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
s.add(
Layout::open_to_the_left_big,
"Layout - Open To The Left - Big",
);
s.add(GradientAngle::new, "Gradient - Angle");
s.add(GradientArea::new, "Gradient - Area");
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);
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
anim_control_bar.add_css_class("anim-control-bar");
anim_control_bar.append(&gtk::Label::new(Some("Slowdown")));
anim_control_bar.append(&anim_scale);
let content_view = adw::ToolbarView::new();
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.add_top_bar(&content_headerbar);
content_view.add_bottom_bar(&anim_control_bar);
content_view.set_content(Some(&stack));
let content = adw::NavigationPage::new(
&content_view,
stack
.page(&stack.visible_child().unwrap())
.title()
.as_deref()
.unwrap(),
);
let sidebar_header = adw::HeaderBar::new();
let stack_sidebar = gtk::StackSidebar::new();
stack_sidebar.set_stack(&stack);
let sidebar_view = adw::ToolbarView::new();
sidebar_view.add_top_bar(&sidebar_header);
sidebar_view.set_content(Some(&stack_sidebar));
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
let split_view = adw::NavigationSplitView::new();
split_view.set_content(Some(&content));
split_view.set_sidebar(Some(&sidebar));
stack.connect_visible_child_notify(move |stack| {
content.set_title(
stack
.visible_child()
.and_then(|c| stack.page(&c).title())
.as_deref()
.unwrap_or_default(),
)
});
let window = adw::ApplicationWindow::new(app);
window.set_title(Some("niri visual tests"));
window.set_content(Some(&split_view));
window.present();
}
+250
View File
@@ -0,0 +1,250 @@
use gtk::glib;
use gtk::subclass::prelude::*;
use smithay::utils::{Logical, Size};
use crate::cases::TestCase;
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::render_helpers::shaders;
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
#[derive(Default)]
pub struct SmithayView {
gl_area: gtk::GLArea,
size: Cell<(i32, i32)>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SmithayView {
const NAME: &'static str = "NiriSmithayView";
type Type = super::SmithayView;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk::BinLayout>();
}
}
impl ObjectImpl for SmithayView {
fn constructed(&self) {
let obj = self.obj();
self.parent_constructed();
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
self.gl_area.set_parent(&*obj);
self.gl_area.connect_resize({
let imp = self.downgrade();
move |_, width, height| {
if let Some(imp) = imp.upgrade() {
imp.resize(width, height);
}
}
});
self.gl_area.connect_render({
let imp = self.downgrade();
move |_, gl_context| {
if let Some(imp) = imp.upgrade() {
if let Err(err) = imp.render(gl_context) {
warn!("error rendering: {err:?}");
}
}
glib::Propagation::Stop
}
});
obj.add_tick_callback(|obj, _frame_clock| {
let imp = obj.imp();
if let Some(case) = &mut *imp.test_case.borrow_mut() {
if case.are_animations_ongoing() {
imp.gl_area.queue_draw();
}
}
glib::ControlFlow::Continue
});
}
fn dispose(&self) {
self.gl_area.unparent();
}
}
impl WidgetImpl for SmithayView {
fn unmap(&self) {
self.test_case.replace(None);
self.parent_unmap();
}
fn unrealize(&self) {
self.renderer.replace(None);
self.parent_unrealize();
}
}
impl SmithayView {
fn resize(&self, width: i32, height: i32) {
self.size.set((width, height));
if let Some(case) = &mut *self.test_case.borrow_mut() {
case.resize(width, height);
}
}
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
// Set up the Smithay renderer.
let mut renderer = self.renderer.borrow_mut();
let renderer = renderer.get_or_insert_with(|| {
unsafe { create_renderer() }
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
});
let Ok(renderer) = renderer else {
return Ok(());
};
let size = self.size.get();
// 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))
});
case.advance_animations(get_monotonic_time());
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
case.render(renderer, Size::from(size))
})
}?;
let mut frame = renderer
.render(rect.size, Transform::Normal)
.context("error creating frame")?;
frame
.clear([0.3, 0.3, 0.3, 1.], &[rect])
.context("error clearing")?;
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
Ok(())
}
}
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
let egl_display = egl::GetCurrentDisplay();
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
let egl_context = egl::GetCurrentContext();
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
// really use it anyway so just get some random one.
let mut egl_config_id = null();
let mut num_configs = 0;
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
ensure!(res == egl::TRUE, "error choosing EGL config");
ensure!(num_configs != 0, "no EGL config");
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
.context("error creating EGL context")?;
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
.context("error getting supported renderer capabilities")?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")?;
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)
}
}
glib::wrapper! {
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
@extends gtk::Widget;
}
impl SmithayView {
pub fn new<T: TestCase + 'static>(
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
) -> Self {
let obj: Self = glib::Object::builder().build();
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
let make_test_case = Box::new(make) as _;
let _ = obj.imp().make_test_case.set(make_test_case);
obj
}
}
+208
View File
@@ -0,0 +1,208 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement};
use niri::render_helpers::renderer::NiriRenderer;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
id: usize,
size: Size<i32, Logical>,
requested_size: Option<Size<i32, Logical>>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
buffer: SolidColorBuffer,
pending_fullscreen: bool,
csd_shadow_width: i32,
csd_shadow_buffer: SolidColorBuffer,
}
#[derive(Debug, Clone)]
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
impl TestWindow {
pub fn freeform(id: usize) -> Self {
let size = Size::from((100, 200));
let min_size = Size::from((0, 0));
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
Self(Rc::new(RefCell::new(TestWindowInner {
id,
size,
requested_size: None,
min_size,
max_size,
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
})))
}
pub fn fixed_size(id: usize) -> Self {
let rv = Self::freeform(id);
rv.set_min_size((200, 400).into());
rv.set_max_size((200, 400).into());
rv.set_color([0.88, 0.11, 0.14, 1.]);
rv.communicate();
rv
}
pub fn set_min_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().max_size = size;
}
pub fn set_color(&self, color: [f32; 4]) {
self.0.borrow_mut().buffer.set_color(color);
}
pub fn set_csd_shadow_width(&self, width: i32) {
self.0.borrow_mut().csd_shadow_width = width;
}
pub fn communicate(&self) -> bool {
let mut rv = false;
let mut inner = self.0.borrow_mut();
let mut new_size = inner.size;
if let Some(size) = inner.requested_size.take() {
assert!(size.w >= 0);
assert!(size.h >= 0);
if size.w != 0 {
new_size.w = size.w;
}
if size.h != 0 {
new_size.h = size.h;
}
}
if inner.max_size.w > 0 {
new_size.w = min(new_size.w, inner.max_size.w);
}
if inner.max_size.h > 0 {
new_size.h = min(new_size.h, inner.max_size.h);
}
if inner.min_size.w > 0 {
new_size.w = max(new_size.w, inner.min_size.w);
}
if inner.min_size.h > 0 {
new_size.h = max(new_size.h, inner.min_size.h);
}
if inner.size != new_size {
inner.size = new_size;
inner.buffer.resize(new_size);
rv = true;
}
let mut csd_shadow_size = new_size;
csd_shadow_size.w += inner.csd_shadow_width * 2;
csd_shadow_size.h += inner.csd_shadow_width * 2;
inner.csd_shadow_buffer.resize(csd_shadow_size);
rv
}
}
impl PartialEq for TestWindow {
fn eq(&self, other: &Self) -> bool {
self.0.borrow().id == other.0.borrow().id
}
}
impl LayoutElement for TestWindow {
fn size(&self) -> Size<i32, Logical> {
self.0.borrow().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
(0, 0).into()
}
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
false
}
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
let inner = self.0.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into(),
]
}
fn request_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().requested_size = Some(size);
self.0.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
self.0.borrow().max_size
}
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
false
}
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
fn has_ssd(&self) -> bool {
false
}
fn output_enter(&self, _output: &Output) {}
fn output_leave(&self, _output: &Output) {}
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.borrow().pending_fullscreen
}
}
+240 -15
View File
@@ -28,6 +28,7 @@ input {
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
@@ -40,6 +41,12 @@ input {
// accel-profile "flat"
}
trackpoint {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
tablet {
// Set the name of the output (see below) which the tablet will map to.
// If this is unset or the output doesn't exist, the tablet maps to one of the
@@ -47,6 +54,13 @@ input {
map-to-output "eDP-1"
}
touch {
// Set the name of the output (see below) which touch input will map to.
// If this is unset or the output doesn't exist, touch input maps to one of the
// existing outputs.
map-to-output "eDP-1"
}
// By default, niri will take over the power button to make it sleep
// instead of power off.
// Uncomment this if you would like to configure the power button elsewhere
@@ -57,7 +71,7 @@ input {
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Remember to uncommend the node by removing "/-"!
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
// off
@@ -65,13 +79,17 @@ input {
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// Transform allows to rotate the output counter-clockwise, valid values are:
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
transform "normal"
// Resolution and, optionally, refresh rate of the output.
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
// If the refresh rate is omitted, niri will pick the highest refresh rate
// for the resolution.
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@144"
mode "1920x1080@120.030"
// Position of the output in the global coordinate space.
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
@@ -86,6 +104,14 @@ input {
}
layout {
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
@@ -94,11 +120,33 @@ layout {
// How many logical pixels the ring extends out from the windows.
width 4
// Color of the ring on the active monitor: red, green, blue, alpha.
active-color 127 200 255 255
// Colors can be set in a variety of ways:
// - CSS named colors: "red"
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
// Color of the ring on the active monitor.
active-color "#7fc8ff"
// Color of the ring on inactive monitors.
inactive-color "#505050"
// Additionally, there's a legacy RGBA syntax:
// active-color 127 200 255 255
// You can also use gradients. They take precedence over solid colors.
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
// The angle is the same as in linear-gradient, and is optional,
// defaulting to 180 (top-to-bottom gradient).
// You can use any CSS linear-gradient tool on the web to set these up.
//
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// You can also color the gradient relative to the entire view
// of the workspace, rather than relative to just the window itself.
// To do that, set relative-to="workspace-view".
//
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can also add a border. It's similar to the focus ring, but always visible.
@@ -108,8 +156,11 @@ layout {
off
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
active-color "#ffc87f"
inactive-color "#505050"
// 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 customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
@@ -117,9 +168,9 @@ layout {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.33333
proportion 0.5
proportion 0.667
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
@@ -159,6 +210,15 @@ layout {
// which may be more convenient to use.
// spawn-at-startup "alacritty" "-e" "fish"
// You can override environment variables for processes spawned by niri.
environment {
// Set a variable like this:
// QT_QPA_PLATFORM "wayland"
// Remove a variable by using null as the value:
// DISPLAY null
}
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
@@ -185,6 +245,139 @@ hotkey-overlay {
// skip-at-startup
}
// Animation settings.
animations {
// Uncomment to turn off all animations.
// off
// Slow down all animations by this factor. Values below 1 speed them up instead.
// slowdown 3.0
// You can configure all individual animations.
// Available settings are the same for all of them.
// - off disables the animation.
//
// Niri supports two animation types: easing and spring.
// You can set properties for only ONE of them.
//
// Easing has the following settings:
// - duration-ms sets the duration of the animation in milliseconds.
// - curve sets the easing curve. Currently, available curves
// are "ease-out-cubic" and "ease-out-expo".
//
// Spring animations work better with touchpad gestures, because they
// take into account the velocity of your fingers as you release the swipe.
// The parameters are less obvious and generally should be tuned
// with trial and error. Notably, you cannot directly set the duration.
// You can use this app to help visualize how the spring parameters
// change the animation: https://flathub.org/apps/app.drey.Elastic
//
// A spring animation is configured like this:
// - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
//
// The damping ratio goes from 0.1 to 10.0 and has the following properties:
// - below 1.0: underdamped spring, will oscillate in the end.
// - above 1.0: overdamped spring, won't oscillate.
// - 1.0: critically damped spring, comes to rest in minimum possible time
// without oscillations.
//
// However, even with damping ratio = 1.0 the spring animation may oscillate
// if "launched" with enough velocity from a touchpad swipe.
//
// Lower stiffness will result in a slower animation more prone to oscillation.
//
// Set epsilon to a lower value if the animation "jumps" in the end.
//
// The spring mass is hardcoded to 1.0 and cannot be changed. Instead, change
// stiffness proportionally. E.g. increasing mass by 2x is the same as
// decreasing stiffness by 2x.
// Animation when switching workspaces up and down,
// including after the touchpad gesture.
workspace-switch {
// off
// spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
// All horizontal camera view movement:
// - When a window off-screen is focused and the camera scrolls to it.
// - When a new window appears off-screen and the camera scrolls to it.
// - When a window resizes bigger and the camera scrolls to show it in full.
// - And so on.
horizontal-view-movement {
// off
// spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
// Window opening animation. Note that this one has different defaults.
window-open {
// off
// duration-ms 150
// curve "ease-out-expo"
// Example for a slightly bouncy window opening:
// spring damping-ratio=0.8 stiffness=1000 epsilon=0.0001
}
// Config parse error and new default config creation notification
// open/close animation.
config-notification-open-close {
// off
// spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
// Window rules let you adjust behavior for individual windows.
// They are processed in order of appearance in this file.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
// Match directives control which windows this rule will apply to.
// You can match by app-id and by title.
// The window must match all properties of the match directive.
match app-id="org.myapp.MyApp" title="My Cool App"
// There can be multiple match directives. A window must match any one
// of the rule's match directives.
//
// If there are no match directives, any window will match the rule.
match title="Second App"
// You can also add exclude directives which have the same properties.
// If a window matches any exclude directive, it won't match this rule.
//
// Both app-id and title are regular expressions.
// Raw KDL strings are helpful here.
exclude app-id=r#"\.unwanted\."#
// Here are the properties that you can set on a window rule.
// You can override the default column width.
default-column-width { proportion 0.75; }
// You can set the output that this window will initially open on.
// If such an output does not exist, it will open on the currently
// focused output as usual.
open-on-output "eDP-1"
// Make this window open as a maximized column.
open-maximized true
// Make this window open fullscreen.
open-fullscreen true
// You can also set this to false to prevent a window from opening fullscreen.
// open-fullscreen false
}
// Here's a useful example. Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
// The regular expression can match anywhere in the string.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
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
@@ -192,6 +385,9 @@ binds {
//
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// when running as a winit window.
//
// Most actions that you can bind here can also be invoked programmatically with
// `niri msg action do-something`.
// Mod-Shift-/, which is usually the same as Mod-?,
// shows a list of important hotkeys.
@@ -200,7 +396,7 @@ binds {
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Mod+Alt+L { spawn "swaylock"; }
Super+Alt+L { spawn "swaylock"; }
// You can also use a shell:
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
@@ -263,6 +459,10 @@ binds {
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
// ...
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
@@ -281,6 +481,14 @@ binds {
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// You can refer to workspaces by index. However, keep in mind that
// niri is a dynamic workspace system, so these commands are kind of
// "best effort". Trying to refer to a workspace index bigger than
// the current workspace count will instead refer to the bottommost
// (empty) workspace.
//
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
// will all refer to the 3rd workspace.
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
@@ -306,6 +514,10 @@ binds {
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.
// Mod+BracketLeft { consume-or-expel-window-left; }
// Mod+BracketRight { consume-or-expel-window-right; }
Mod+R { switch-preset-column-width; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
@@ -338,10 +550,17 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// The quit action will show a confirmation dialog to avoid accidental exits.
// If you want to skip the confirmation dialog, set the flag like so:
// Mod+Shift+E { quit skip-confirmation=true; }
Mod+Shift+E { quit; }
Mod+Shift+P { power-off-monitors; }
Mod+Shift+Ctrl+T { toggle-debug-tint; }
// This debug bind will tint all surfaces green, unless they are being
// directly scanned out. It's therefore useful to check if direct scanout
// is working.
// Mod+Shift+Ctrl+T { toggle-debug-tint; }
}
// Settings for debugging. Not meant for normal use.
@@ -364,9 +583,15 @@ debug {
// The cursor will be rendered together with the rest of the frame.
// disable-cursor-plane
// Slow down animations by this factor.
// animation-slowdown 3.0
// Override the DRM device that niri will use for all rendering.
// render-drm-device "/dev/dri/renderD129"
// Enable the color-transformations capability of the Smithay renderer.
// May cause a slight decrease in rendering performance.
// enable-color-transformations-capability
// Emulate zero (unknown) presentation time returned from DRM.
// This is a thing on NVIDIA proprietary drivers, so this flag can be
// used to test that we don't break too hard on those systems.
// emulate-zero-presentation-time
}
+1 -7
View File
@@ -20,12 +20,6 @@ fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Set the current desktop for xdg-desktop-portal.
export XDG_CURRENT_DESKTOP=niri
# Ensure the session type is set to Wayland for xdg-autostart apps.
export XDG_SESSION_TYPE=wayland
# Import the login manager environment.
systemctl --user import-environment
@@ -44,4 +38,4 @@ systemctl --user --wait start niri.service
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
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
+2 -1
View File
@@ -9,5 +9,6 @@ Wants=xdg-desktop-autostart.target
Before=xdg-desktop-autostart.target
[Service]
Slice=session.slice
Type=notify
ExecStart=/usr/bin/niri
ExecStart=/usr/bin/niri --session
-58
View File
@@ -1,58 +0,0 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
duration: Duration,
start_time: Duration,
current_time: Duration,
}
impl Animation {
pub fn new(from: f64, to: f64, over: Duration) -> 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();
Self {
from,
to,
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
start_time: now,
current_time: now,
}
}
pub fn set_current_time(&mut self, time: Duration) {
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn value(&self) -> f64 {
let passed = (self.current_time - self.start_time).as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
EaseOutCubic.y(x) * (self.to - self.from) + self.from
}
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
}
+282
View File
@@ -0,0 +1,282 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
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.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
duration: Duration,
start_time: Duration,
current_time: Duration,
kind: Kind,
}
#[derive(Debug, Clone, Copy)]
enum Kind {
Easing {
curve: Curve,
},
Spring(Spring),
Deceleration {
initial_velocity: f64,
deceleration_rate: f64,
},
}
#[derive(Debug, Clone, Copy)]
pub enum Curve {
EaseOutCubic,
EaseOutExpo,
}
impl Animation {
pub fn new(
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
default: niri_config::Animation,
) -> Self {
if config.off {
return Self::ease(from, to, 0, Curve::EaseOutCubic);
}
// Resolve defaults.
let (kind, easing_defaults) = match (config.kind, default.kind) {
// Configured spring.
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
// Configured nothing, defaults spring.
(
niri_config::AnimationKind::Easing(easing),
defaults @ niri_config::AnimationKind::Spring(_),
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
// Configured easing or nothing, defaults easing.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Easing(defaults),
) => (configured, Some(defaults)),
// Configured easing, defaults spring.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Spring(_),
) => (configured, None),
};
match kind {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
let spring = Spring {
from,
to,
initial_velocity,
params,
};
Self::spring(spring)
}
niri_config::AnimationKind::Easing(p) => {
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
Self::ease(from, to, u64::from(duration_ms), curve)
}
}
}
pub fn ease(from: f64, to: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
Self {
from,
to,
duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn spring(spring: Spring) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let kind = Kind::Spring(spring);
Self {
from: spring.from,
to: spring.to,
duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn decelerate(
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 {
let coeff = 1000. * deceleration_rate.ln();
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
};
let duration = Duration::from_secs_f64(duration_s);
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
let kind = Kind::Deceleration {
initial_velocity,
deceleration_rate,
};
Self {
from,
to,
duration,
start_time: now,
current_time: now,
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
}
pub fn value(&self) -> f64 {
if self.is_done() {
return self.to;
}
let passed = self.current_time - self.start_time;
match self.kind {
Kind::Easing { curve } => {
let passed = passed.as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
curve.y(x) * (self.to - self.from) + self.from
}
Kind::Spring(spring) => spring.value_at(passed),
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let passed = passed.as_secs_f64();
let coeff = 1000. * deceleration_rate.ln();
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
}
}
}
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
}
impl Curve {
pub fn y(self, x: f64) -> f64 {
match self {
Curve::EaseOutCubic => EaseOutCubic.y(x),
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
}
}
}
impl From<niri_config::AnimationCurve> for Curve {
fn from(value: niri_config::AnimationCurve) -> Self {
match value {
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
}
}
}
+137
View File
@@ -0,0 +1,137 @@
use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct SpringParams {
pub damping: f64,
pub mass: f64,
pub stiffness: f64,
pub epsilon: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct Spring {
pub from: f64,
pub to: f64,
pub initial_velocity: f64,
pub params: SpringParams,
}
impl SpringParams {
pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self {
let damping_ratio = damping_ratio.max(0.);
let stiffness = stiffness.max(0.);
let epsilon = epsilon.max(0.);
let mass = 1.;
let critical_damping = 2. * (mass * stiffness).sqrt();
let damping = damping_ratio * critical_damping;
Self {
damping,
mass,
stiffness,
epsilon,
}
}
}
impl Spring {
pub fn value_at(&self, t: Duration) -> f64 {
self.oscillate(t.as_secs_f64())
}
// Based on libadwaita (LGPL-2.1-or-later):
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c,
// which itself is based on (MIT):
// https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
/// Computes and returns the duration until the spring is at rest.
pub fn duration(&self) -> Duration {
const DELTA: f64 = 0.001;
let beta = self.params.damping / (2. * self.params.mass);
if beta.abs() <= f64::EPSILON || beta < 0. {
return Duration::MAX;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
// and general estimation for the oscillating ones
// we take the value of the envelope when it's < epsilon.
let mut x0 = -self.params.epsilon.ln() / beta;
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
return Duration::from_secs_f64(x0);
}
// Since the overdamped solution decays way slower than the envelope
// we need to use the value of the oscillation itself.
// Newton's root finding method is a good candidate in this particular case:
// https://en.wikipedia.org/wiki/Newton%27s_method
let mut y0 = self.oscillate(x0);
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
let mut x1 = (self.to - y0 + m * x0) / m;
let mut y1 = self.oscillate(x1);
let mut i = 0;
while (self.to - y1).abs() > self.params.epsilon {
if i > 1000 {
return Duration::ZERO;
}
x0 = x1;
y0 = y1;
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
i += 1;
}
Duration::from_secs_f64(x1)
}
/// Returns the spring position at a given time in seconds.
fn oscillate(&self, t: f64) -> f64 {
let b = self.params.damping;
let m = self.params.mass;
let k = self.params.stiffness;
let v0 = self.initial_velocity;
let beta = b / (2. * m);
let omega0 = (k / m).sqrt();
let x0 = self.from - self.to;
let envelope = (-beta * t).exp();
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
// for the differential equation m*ẍ+b*ẋ+kx = 0
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
// Critically damped.
self.to + envelope * (x0 + (beta * x0 + v0) * t)
} else if beta < omega0 {
// Underdamped.
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
self.to
+ envelope
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
} else {
// Overdamped.
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
self.to
+ envelope
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
}
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::Niri;
use crate::niri::Niri;
pub mod tty;
pub use tty::Tty;
@@ -98,7 +98,7 @@ impl Backend {
}
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
@@ -138,7 +138,7 @@ impl Backend {
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
+363 -175
View File
@@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -10,7 +11,7 @@ use std::{io, mem};
use anyhow::{anyhow, Context};
use libc::dev_t;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::{Format, Fourcc};
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
@@ -20,7 +21,7 @@ use smithay::backend::drm::{
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -28,7 +29,7 @@ use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
use smithay::desktop::utils::OutputPresentationFeedback;
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel};
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties};
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
use smithay::reexports::drm::control::{
@@ -41,6 +42,9 @@ use smithay::reexports::wayland_protocols;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::DeviceFd;
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
@@ -48,10 +52,10 @@ use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use super::RenderResult;
use crate::frame_clock::FrameClock;
use crate::niri::{RedrawState, State};
use crate::render_helpers::AsGlesRenderer;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
use crate::Niri;
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -60,7 +64,7 @@ pub struct Tty {
session: LibSeatSession,
udev_dispatcher: Dispatcher<'static, UdevBackend, State>,
libinput: Libinput,
gpu_manager: GpuManager<GbmGlesBackend<GlesRenderer>>,
gpu_manager: GpuManager<GbmGlesBackend<GlesRenderer, DrmDeviceFd>>,
// DRM node corresponding to the primary GPU. May or may not be the same as
// primary_render_node.
primary_node: DrmNode,
@@ -71,31 +75,30 @@ pub struct Tty {
// The dma-buf global corresponds to the output device (the primary GPU). It is only `Some()`
// if we have a device corresponding to the primary GPU.
dmabuf_global: Option<DmabufGlobal>,
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
// the primary GPU.
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
// Whether the debug tinting is enabled.
debug_tint: bool,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
pub type TtyRenderer<'render> = MultiRenderer<
'render,
'render,
'alloc,
GbmGlesBackend<GlesRenderer>,
GbmGlesBackend<GlesRenderer>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyFrame<'render, 'alloc, 'frame> = MultiFrame<
pub type TtyFrame<'render, 'frame> = MultiFrame<
'render,
'render,
'alloc,
'frame,
GbmGlesBackend<GlesRenderer>,
GbmGlesBackend<GlesRenderer>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
pub type TtyRendererError<'render, 'alloc> = <TtyRenderer<'render, 'alloc> as Renderer>::Error;
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
type GbmDrmCompositor = DrmCompositor<
GbmAllocator<DrmDeviceFd>,
@@ -104,7 +107,7 @@ type GbmDrmCompositor = DrmCompositor<
DrmDeviceFd,
>;
struct OutputDevice {
pub struct OutputDevice {
token: RegistrationToken,
render_node: DrmNode,
drm_scanner: DrmScanner,
@@ -113,6 +116,42 @@ struct OutputDevice {
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
gbm: GbmDevice<DrmDeviceFd>,
pub drm_lease_state: DrmLeaseState,
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
active_leases: Vec<DrmLease>,
}
impl OutputDevice {
pub fn lease_request(
&self,
request: DrmLeaseRequest,
) -> Result<DrmLeaseBuilder, LeaseRejected> {
let mut builder = DrmLeaseBuilder::new(&self.drm);
for connector in request.connectors {
let (_, crtc) = self
.non_desktop_connectors
.iter()
.find(|(conn, _)| connector == *conn)
.ok_or_else(|| {
warn!("Attempted to lease connector that is not non-desktop");
LeaseRejected::default()
})?;
builder.add_connector(connector);
builder.add_crtc(*crtc);
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
builder.add_plane(planes.primary.handle);
}
Ok(builder)
}
pub fn new_lease(&mut self, lease: DrmLease) {
self.active_leases.push(lease);
}
pub fn remove_lease(&mut self, lease_id: u32) {
self.active_leases.retain(|l| l.id() != lease_id);
}
}
#[derive(Debug, Clone, Copy)]
@@ -142,11 +181,19 @@ pub struct SurfaceDmabufFeedback {
}
impl Tty {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
let (session, notifier) = LibSeatSession::new().unwrap();
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<'static, State>,
) -> anyhow::Result<Self> {
let (session, notifier) = LibSeatSession::new().context(
"Error creating a session. This might mean that you're trying to run niri on a TTY \
that is already busy, for example if you're running this inside tmux that had been \
originally started on a different TTY",
)?;
let seat_name = session.seat();
let udev_backend = UdevBackend::new(session.seat()).unwrap();
let udev_backend =
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
state.backend.tty().on_udev_event(&mut state.niri, event);
});
@@ -155,7 +202,9 @@ impl Tty {
.unwrap();
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput.udev_assign_seat(&seat_name).unwrap();
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
@@ -190,18 +239,23 @@ impl Tty {
Ok(gles)
};
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
let gpu_manager = GpuManager::new(api).unwrap();
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
.unwrap_or_else(|| {
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
.ok_or(())
.or_else(|()| {
let primary_gpu_path = udev::primary_gpu(&seat_name)
.context("error getting the primary GPU")?
.context("couldn't find a GPU")?;
let primary_node = DrmNode::from_path(primary_gpu_path)
.context("error opening the primary GPU DRM node")?;
let primary_render_node = primary_node
.node_with_type(NodeType::Render)
.unwrap()
.unwrap();
(primary_node, primary_render_node)
});
.context("error getting the render node for the primary GPU")?
.context("error getting the render node for the primary GPU")?;
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
})?;
let mut node_path = String::new();
if let Some(path) = primary_render_node.dev_path() {
@@ -211,7 +265,7 @@ impl Tty {
}
info!("using as the render node: {}", node_path);
Self {
Ok(Self {
config,
session,
udev_dispatcher,
@@ -221,10 +275,11 @@ impl Tty {
primary_render_node,
devices: HashMap::new(),
dmabuf_global: None,
primary_allocator: None,
update_output_config_on_resume: false,
debug_tint: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
}
})
}
pub fn init(&mut self, niri: &mut Niri) {
@@ -277,7 +332,7 @@ impl Tty {
self.libinput.suspend();
for device in self.devices.values() {
for device in self.devices.values_mut() {
device.drm.pause();
}
}
@@ -285,7 +340,7 @@ impl Tty {
debug!("resuming session");
if self.libinput.resume().is_err() {
error!("error resuming libinput");
warn!("error resuming libinput");
}
let mut device_list = self
@@ -320,47 +375,13 @@ impl Tty {
device_list.remove(&node.dev_id());
// It hasn't been removed, update its state as usual.
let device = &self.devices[&node];
device.drm.activate();
// HACK: force reset the connectors to make resuming work across sleep.
let device = &self.devices[&node];
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(_conn, crtc)| crtc)
.collect();
for crtc in crtcs {
self.connector_disconnected(niri, node, crtc);
}
let device = self.devices.get_mut(&node).unwrap();
let _ = device.drm_scanner.scan_connectors(&device.drm);
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
warn!("error connecting connector: {err:?}");
}
if let Err(err) = device.drm.activate(true) {
warn!("error activating DRM device: {err:?}");
}
// // Refresh the connectors.
// self.device_changed(node.dev_id(), niri);
// // Refresh the state on unchanged connectors.
// let device = self.devices.get_mut(&node).unwrap();
// for surface in device.surfaces.values_mut() {
// let compositor = &mut surface.compositor;
// if let Err(err) = compositor.surface().reset_state() {
// warn!("error resetting DRM surface state: {err}");
// }
// compositor.reset_buffers();
// }
// niri.queue_redraw_all();
// Refresh the connectors.
self.device_changed(node.dev_id(), niri);
}
// Add new devices.
@@ -370,7 +391,16 @@ impl Tty {
}
}
if self.update_output_config_on_resume {
self.on_output_config_changed(niri);
}
self.refresh_ipc_outputs();
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.monitors_active = true;
self.set_monitors_active(true);
niri.queue_redraw_all();
}
}
}
@@ -384,6 +414,8 @@ impl Tty {
debug!("device added: {device_id} {path:?}");
let node = DrmNode::from_dev_id(device_id)?;
let drm_lease_state = DrmLeaseState::new::<State>(&niri.display_handle, &node)
.context("Couldn't create DrmLeaseState")?;
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK;
let fd = self.session.open(path, open_flags)?;
@@ -392,15 +424,9 @@ impl Tty {
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
let gbm = GbmDevice::new(device_fd)?;
let display = EGLDisplay::new(gbm.clone())?;
let display = unsafe { EGLDisplay::new(gbm.clone())? };
let egl_device = EGLDevice::device_for_display(&display)?;
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
// same as the one we just created here, so when ours is dropped at the end of the scope,
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
// beforehand.
drop(display);
let render_node = egl_device
.try_get_render_node()?
.context("no render node")?;
@@ -419,6 +445,8 @@ impl Tty {
renderer.bind_wl_display(&niri.display_handle)?;
shaders::init(renderer.as_gles_renderer());
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats().collect::<HashSet<_>>();
let default_feedback =
@@ -433,11 +461,6 @@ impl Tty {
);
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
// Create the primary allocator.
let primary_allocator =
DmabufAllocator(GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING));
assert!(self.primary_allocator.replace(primary_allocator).is_none());
// Update the dmabuf feedbacks for all surfaces.
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
@@ -467,7 +490,7 @@ impl Tty {
let meta = meta.expect("VBlank events must have metadata");
tty.on_vblank(&mut state.niri, node, crtc, meta);
}
DrmEvent::Error(error) => error!("DRM error: {error}"),
DrmEvent::Error(error) => warn!("DRM error: {error}"),
};
})
.unwrap();
@@ -479,6 +502,9 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
};
assert!(self.devices.insert(node, device).is_none());
@@ -549,7 +575,7 @@ impl Tty {
match self.gpu_manager.single_renderer(&device.render_node) {
Ok(mut renderer) => renderer.unbind_wl_display(),
Err(err) => {
error!("error creating renderer during device removal: {err}");
warn!("error creating renderer during device removal: {err}");
}
}
@@ -570,8 +596,6 @@ impl Tty {
)
.unwrap();
self.primary_allocator = None;
// Clear the dmabuf feedbacks for all surfaces.
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
@@ -600,6 +624,41 @@ impl Tty {
);
debug!("connecting connector: {output_name}");
let device = self.devices.get_mut(&node).context("missing device")?;
let non_desktop = device
.drm
.get_properties(connector.handle())
.ok()
.and_then(|props| {
let (info, value) = props
.into_iter()
.filter_map(|(handle, value)| {
let info = device.drm.get_property(handle).ok()?;
Some((info, value))
})
.find(|(info, _)| info.name().to_str() == Ok("non-desktop"))?;
info.value_type().convert_value(value).as_boolean()
})
.unwrap_or(false);
if non_desktop {
debug!("output is non desktop");
let description = get_edid_info(&device.drm, connector.handle())
.map(|info| truncate_to_nul(info.model))
.unwrap_or_else(|| "Unknown".into());
device.drm_lease_state.add_connector::<State>(
connector.handle(),
output_name,
description,
);
device
.non_desktop_connectors
.insert((connector.handle(), crtc));
return Ok(());
}
let config = self
.config
.borrow()
@@ -614,8 +673,6 @@ impl Tty {
return Ok(());
}
let device = self.devices.get_mut(&node).context("missing device")?;
for m in connector.modes() {
trace!("{m:?}");
}
@@ -648,15 +705,20 @@ impl Tty {
// Update the output mode.
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
.map(|info| (info.manufacturer, info.model))
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output = Output::new(
output_name.clone(),
PhysicalProperties {
size: (physical_width as i32, physical_height as i32).into(),
subpixel: Subpixel::Unknown,
subpixel: connector.subpixel().into(),
model,
make,
},
@@ -692,7 +754,7 @@ impl Tty {
let render_formats = egl_context.dmabuf_render_formats();
// Create the compositor.
let compositor = DrmCompositor::new(
let mut compositor = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
@@ -705,6 +767,9 @@ impl Tty {
device.drm.cursor_size(),
cursor_plane_gbm,
)?;
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
let mut dmabuf_feedback = None;
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
@@ -735,13 +800,8 @@ impl Tty {
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
let surface = Surface {
name: output_name,
name: output_name.clone(),
compositor,
dmabuf_feedback,
vblank_frame: None,
@@ -755,9 +815,16 @@ impl Tty {
niri.add_output(output.clone(), Some(refresh_interval(mode)));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name, output.clone());
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&state.backend);
state.niri.activate_monitors(&mut state.backend);
state.niri.queue_redraw(output);
});
@@ -794,6 +861,8 @@ impl Tty {
};
self.enabled_outputs.lock().unwrap().remove(&surface.name);
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
}
fn on_vblank(
@@ -834,6 +903,11 @@ impl Tty {
Duration::ZERO
}
};
let presentation_time = if niri.config.borrow().debug.emulate_zero_presentation_time {
Duration::ZERO
} else {
presentation_time
};
let message = if presentation_time.is_zero() {
format!("vblank on {name}, presentation time unknown")
@@ -883,16 +957,17 @@ impl Tty {
.unwrap_or(Duration::ZERO);
// FIXME: ideally should be monotonically increasing for a surface.
let seq = meta.sequence as u64;
let flags = wp_presentation_feedback::Kind::Vsync
| wp_presentation_feedback::Kind::HwClock
let mut flags = wp_presentation_feedback::Kind::Vsync
| wp_presentation_feedback::Kind::HwCompletion;
feedback.presented::<_, smithay::utils::Monotonic>(
presentation_time,
refresh,
seq,
flags,
);
let time = if presentation_time.is_zero() {
now
} else {
flags.insert(wp_presentation_feedback::Kind::HwClock);
presentation_time
};
feedback.presented::<_, smithay::utils::Monotonic>(time, refresh, seq, flags);
if !presentation_time.is_zero() {
let misprediction_s =
@@ -905,19 +980,19 @@ impl Tty {
}
Ok(None) => (),
Err(err) => {
error!("error marking frame as submitted: {err}");
warn!("error marking frame as submitted: {err}");
}
}
if let Some(last_sequence) = output_state.current_estimated_sequence {
if let Some(last_sequence) = output_state.last_drm_sequence {
let delta = meta.sequence as f64 - last_sequence as f64;
tracy_client::Client::running()
.unwrap()
.plot(surface.sequence_delta_plot_name, delta);
}
output_state.last_drm_sequence = Some(meta.sequence);
output_state.frame_clock.presented(presentation_time);
output_state.current_estimated_sequence = Some(meta.sequence);
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
@@ -950,6 +1025,9 @@ impl Tty {
return;
};
// We waited for the timer, now we can send frame callbacks again.
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => unreachable!(),
@@ -962,14 +1040,10 @@ impl Tty {
}
}
if let Some(sequence) = output_state.current_estimated_sequence.as_mut() {
*sequence = sequence.wrapping_add(1);
if output_state.unfinished_animations_remain {
niri.queue_redraw(output);
} else {
niri.send_frame_callbacks(&output);
}
if output_state.unfinished_animations_remain {
niri.queue_redraw(output);
} else {
niri.send_frame_callbacks(&output);
}
}
@@ -1016,20 +1090,14 @@ impl Tty {
return rv;
}
let Some(allocator) = self.primary_allocator.as_mut() else {
warn!("no primary allocator");
return rv;
};
let mut renderer = match self.gpu_manager.renderer(
&self.primary_render_node,
&device.render_node,
allocator,
surface.compositor.format(),
) {
Ok(renderer) => renderer,
Err(err) => {
error!("error creating renderer for primary GPU: {err:?}");
warn!("error creating renderer for primary GPU: {err:?}");
return rv;
}
};
@@ -1039,7 +1107,7 @@ impl Tty {
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
@@ -1058,7 +1126,7 @@ impl Tty {
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
}
if res.damage.is_some() {
if !res.is_empty {
let presentation_feedbacks =
niri.take_presentation_feedbacks(output, &res.states);
let data = (presentation_feedbacks, target_presentation_time);
@@ -1079,10 +1147,16 @@ impl Tty {
}
};
// We queued this frame successfully, so the current client buffers were
// latched. We can send frame callbacks now, since a new client commit
// will no longer overwrite this frame and will wait for a VBlank.
output_state.frame_callback_sequence =
output_state.frame_callback_sequence.wrapping_add(1);
return RenderResult::Submitted;
}
Err(err) => {
error!("error queueing frame: {err}");
warn!("error queueing frame: {err}");
}
}
} else {
@@ -1091,7 +1165,7 @@ impl Tty {
}
Err(err) => {
// Can fail if we switched to a different TTY.
error!("error rendering frame: {err}");
warn!("error rendering frame: {err}");
}
}
@@ -1106,7 +1180,7 @@ impl Tty {
pub fn change_vt(&mut self, vt: i32) {
if let Err(err) = self.session.change_vt(vt) {
error!("error changing VT: {err}");
warn!("error changing VT: {err}");
}
}
@@ -1118,36 +1192,42 @@ impl Tty {
}
pub fn toggle_debug_tint(&mut self) {
self.debug_tint = !self.debug_tint;
for device in self.devices.values_mut() {
for surface in device.surfaces.values_mut() {
let compositor = &mut surface.compositor;
compositor.set_debug_flags(compositor.debug_flags() ^ DebugFlags::TINT);
let mut flags = compositor.debug_flags();
flags.set(DebugFlags::TINT, self.debug_tint);
compositor.set_debug_flags(flags);
}
}
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) {
Ok(renderer) => renderer,
Err(err) => {
debug!("error creating renderer for primary GPU: {err:?}");
return Err(());
return false;
}
};
match renderer.import_dmabuf(dmabuf, None) {
Ok(_texture) => Ok(()),
Ok(_texture) => {
dmabuf.set_node(Some(self.primary_render_node));
true
}
Err(err) => {
debug!("error importing dmabuf: {err:?}");
Err(())
false
}
}
}
pub fn early_import(&mut self, surface: &WlSurface) {
if let Err(err) = self.gpu_manager.early_import(
// We always advertise the primary GPU in dmabuf feedback.
Some(self.primary_render_node),
// We always render on the primary GPU.
self.primary_render_node,
surface,
@@ -1171,38 +1251,56 @@ impl Tty {
let physical_size = connector.size();
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
.map(|info| (info.manufacturer, info.model))
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
let mut current_mode = None;
let modes = connector
.modes()
.iter()
.map(|m| niri_ipc::Mode {
width: m.size().0,
height: m.size().1,
refresh_rate: Mode::from(*m).refresh as u32,
.filter(|m| !m.flags().contains(ModeFlags::INTERLACE))
.enumerate()
.map(|(idx, m)| {
if Some(*m) == current_crtc_mode {
current_mode = Some(idx);
}
niri_ipc::Mode {
width: m.size().0,
height: m.size().1,
refresh_rate: Mode::from(*m).refresh as u32,
}
})
.collect();
let mut output = niri_ipc::Output {
if let Some(crtc_mode) = current_crtc_mode {
if current_mode.is_none() {
if crtc_mode.flags().contains(ModeFlags::INTERLACE) {
warn!("connector mode list missing current mode (interlaced)");
} else {
error!("connector mode list missing current mode");
}
}
}
let output = niri_ipc::Output {
name: connector_name.clone(),
make,
model,
physical_size,
modes,
current_mode: None,
current_mode,
};
if let Some(surface) = device.surfaces.get(&crtc) {
let current = surface.compositor.pending_mode();
if let Some(current) = connector.modes().iter().position(|m| *m == current) {
output.current_mode = Some(current);
} else {
error!("connector mode list missing current mode");
}
}
ipc_outputs.insert(connector_name, output);
}
}
@@ -1223,10 +1321,22 @@ impl Tty {
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
}
pub fn set_monitors_active(&self, active: bool) {
for device in self.devices.values() {
for crtc in device.surfaces.keys() {
set_crtc_active(&device.drm, *crtc, active);
pub fn set_monitors_active(&mut self, active: bool) {
// We only disable the CRTC here, this will also reset the
// surface state so that the next call to `render_frame` will
// always produce a new frame and `queue_frame` will change
// the CRTC to active. This makes sure we always enable a CRTC
// within an atomic operation.
if active {
return;
}
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:?}");
}
}
}
}
@@ -1234,6 +1344,13 @@ impl Tty {
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::on_output_config_changed");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_output_config_on_resume = true;
return;
}
self.update_output_config_on_resume = false;
let mut to_disconnect = vec![];
let mut to_connect = vec![];
@@ -1255,23 +1372,22 @@ impl Tty {
}
// Check if we need to change the mode.
let connector = surface
.compositor
.current_connectors()
.into_iter()
.next()
.unwrap();
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
};
let Some((mode, fallback)) = pick_mode(connector, config.mode) else {
error!("couldn't pick mode for enabled connector");
warn!("couldn't pick mode for enabled connector");
continue;
};
if surface.compositor.current_mode() == mode {
if surface.compositor.pending_mode() == mode {
continue;
}
@@ -1365,6 +1481,10 @@ impl Tty {
self.refresh_ipc_outputs();
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
}
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
@@ -1504,9 +1624,17 @@ fn refresh_interval(mode: DrmMode) -> Duration {
#[cfg(feature = "dbus")]
fn suspend() -> anyhow::Result<()> {
let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?;
let manager = logind_zbus::manager::ManagerProxyBlocking::new(&conn)
.context("error creating login manager proxy")?;
manager.suspend(true).context("error suspending")
conn.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Suspend",
&(true),
)
.context("error suspending")?;
Ok(())
}
fn queue_estimated_vblank_timer(
@@ -1527,7 +1655,22 @@ fn queue_estimated_vblank_timer(
}
let now = get_monotonic_time();
let timer = Timer::from_duration(target_presentation_time.saturating_sub(now));
let mut duration = target_presentation_time.saturating_sub(now);
// No use setting a zero timer, since we'll send frame callbacks anyway right after the call to
// render(). This can happen for example with unknown presentation time from DRM.
if duration.is_zero() {
duration += output_state
.frame_clock
.refresh_interval()
// Unknown refresh interval, i.e. winit backend. Would be good to estimate it somehow
// but it's not that important for this code path.
.unwrap_or(Duration::from_micros(16_667));
}
trace!("queueing estimated vblank timer to fire in {duration:?}");
let timer = Timer::from_duration(duration);
let token = niri
.event_loop
.insert_source(timer, move |_, _, data| {
@@ -1555,6 +1698,11 @@ fn pick_mode(
continue;
}
// Interlaced modes don't appear to work.
if m.flags().contains(ModeFlags::INTERLACE) {
continue;
}
if let Some(refresh) = refresh {
// If refresh is set, only pick modes with matching refresh.
let wl_mode = Mode::from(*m);
@@ -1600,3 +1748,43 @@ fn pick_mode(
mode.map(|m| (*m, fallback))
}
fn truncate_to_nul(mut s: String) -> String {
if let Some(index) = s.find('\0') {
s.truncate(index);
}
s
}
fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<EdidInfo> {
match catch_unwind(AssertUnwindSafe(move || {
EdidInfo::for_connector(device, connector)
})) {
Ok(info) => info,
Err(err) => {
warn!("edid-rs panicked: {err:?}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn check(input: &str, expected: &str) {
let input = String::from(input);
assert_eq!(truncate_to_nul(input), expected);
}
#[test]
fn truncate_to_nul_works() {
check("", "");
check("qwer", "qwer");
check("abc\0def", "abc");
check("\0as", "");
check("a\0\0\0b", "a");
check("bb😁\0cc", "bb😁");
}
}
+22 -20
View File
@@ -16,12 +16,11 @@ use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::niri::{RedrawState, State};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
use crate::Niri;
pub struct Winit {
config: Rc<RefCell<Config>>,
@@ -33,12 +32,15 @@ pub struct Winit {
}
impl Winit {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
let (backend, winit) = winit::init_from_builder(builder).unwrap();
let (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -54,7 +56,7 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let physical_properties = output.physical_properties();
@@ -107,32 +109,28 @@ impl Winit {
WinitEvent::Redraw => state
.niri
.queue_redraw(state.backend.winit().output.clone()),
WinitEvent::CloseRequested => {
state.niri.stop_signal.stop();
state.niri.remove_output(&state.backend.winit().output);
}
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
Self {
Ok(Self {
config,
output,
backend,
damage_tracker,
ipc_outputs,
enabled_outputs,
}
})
}
pub fn init(&mut self, niri: &mut Niri) {
if let Err(err) = self
.backend
.renderer()
.bind_wl_display(&niri.display_handle)
{
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
}
shaders::init(renderer);
niri.add_output(self.output.clone(), None);
}
@@ -201,6 +199,10 @@ impl Winit {
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
}
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
// right away instead.
if output_state.unfinished_animations_remain {
self.backend.window().request_redraw();
}
@@ -213,12 +215,12 @@ impl Winit {
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self.backend.renderer().import_dmabuf(dmabuf, None) {
Ok(_texture) => Ok(()),
Ok(_texture) => true,
Err(err) => {
debug!("error importing dmabuf: {err:?}");
Err(())
false
}
}
}
+62
View File
@@ -0,0 +1,62 @@
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use crate::utils::version;
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
pub struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
pub config: Option<PathBuf>,
/// Import environment globally to systemd and D-Bus, run D-Bus services.
///
/// Set this flag in a systemd service started by your display manager, or when running
/// manually as your main compositor instance. Do not set when running as a nested window, or
/// on a TTY as your non-main compositor instance, to avoid messing up the global environment.
#[arg(long)]
pub session: bool,
/// Command to run upon compositor startup.
#[arg(last = true)]
pub command: Vec<OsString>,
#[command(subcommand)]
pub subcommand: Option<Sub>,
}
#[derive(Subcommand)]
pub enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
pub enum Msg {
/// List connected outputs.
Outputs,
/// Perform an action.
Action {
#[command(subcommand)]
action: Action,
},
}
+6 -20
View File
@@ -8,8 +8,7 @@ use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
@@ -224,7 +223,7 @@ pub enum RenderCursor {
},
}
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
#[derive(Default)]
pub struct CursorTextureCache {
@@ -238,12 +237,11 @@ impl CursorTextureCache {
pub fn get(
&self,
renderer: &mut GlesRenderer,
icon: CursorIcon,
scale: i32,
cursor: &XCursor,
idx: usize,
) -> Option<TextureBuffer<GlesTexture>> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,26 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
let buffer = TextureBuffer::from_memory(
renderer,
MemoryRenderBuffer::from_slice(
&frame.pixels_rgba,
Fourcc::Abgr8888,
Fourcc::Argb8888,
(frame.width as i32, frame.height as i32),
false,
scale,
Transform::Normal,
None,
);
match buffer {
Ok(x) => Some(x),
Err(err) => {
warn!("error creating a cursor texture: {err:?}");
None
}
}
)
})
.collect()
})[idx]
+166
View File
@@ -0,0 +1,166 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{dbus_interface, MessageHeader, Task};
use super::Start;
pub struct ScreenSaver {
is_inhibited: Arc<AtomicBool>,
is_broken: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
counter: u32,
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: MessageHeader<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
trace!(
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
hdr.sender()
);
let Ok(Some(name)) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
let mut inhibitors = self.inhibitors.lock().unwrap();
let mut cookie = None;
for _ in 0..3 {
// Start from 1 because some clients don't like 0.
self.counter = self.counter.wrapping_add(1);
if self.counter == 0 {
self.counter += 1;
}
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
entry.insert(name);
self.is_inhibited.store(true, Ordering::SeqCst);
cookie = Some(self.counter);
break;
}
}
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
}
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
trace!("fdo uninhibit, cookie: {cookie}");
let mut inhibitors = self.inhibitors.lock().unwrap();
if inhibitors.remove(&cookie).is_some() {
if inhibitors.is_empty() {
self.is_inhibited.store(false, Ordering::SeqCst);
}
Ok(())
} else {
Err(fdo::Error::Failed(String::from("invalid cookie")))
}
}
}
impl ScreenSaver {
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
Self {
is_inhibited,
is_broken: Arc::new(AtomicBool::new(false)),
inhibitors: Arc::new(Mutex::new(HashMap::new())),
counter: 0,
monitor_task: Arc::new(OnceLock::new()),
}
}
}
async fn monitor_disappeared_clients(
conn: &zbus::Connection,
is_inhibited: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
) -> anyhow::Result<()> {
let proxy = fdo::DBusProxy::new(conn)
.await
.context("error creating a DBusProxy")?;
let mut stream = proxy
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
.await
.context("error creating a NameOwnerChanged stream")?;
while let Some(signal) = stream.next().await {
let args = signal
.args()
.context("error retrieving NameOwnerChanged args")?;
let Some(name) = &**args.old_owner() else {
continue;
};
if args.new_owner().is_none() {
trace!("fdo ScreenSaver client disappeared: {name}");
let mut inhibitors = inhibitors.lock().unwrap();
inhibitors.retain(|_, owner| owner != name);
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
} else {
error!("non-null new_owner should've been filtered out");
}
}
Ok(())
}
impl Start for ScreenSaver {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let is_inhibited = self.is_inhibited.clone();
let is_broken = self.is_broken.clone();
let inhibitors = self.inhibitors.clone();
let monitor_task = self.monitor_task.clone();
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/freedesktop/ScreenSaver", self)?;
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
let async_conn = conn.inner();
let future = {
let conn = async_conn.clone();
async move {
if let Err(err) =
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
.await
{
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
is_broken.store(true, Ordering::SeqCst);
is_inhibited.store(false, Ordering::SeqCst);
inhibitors.lock().unwrap().clear();
}
}
};
let task = async_conn
.executor()
.spawn(future, "monitor disappearing clients");
monitor_task.set(task).unwrap();
Ok(conn)
}
}
-1
View File
@@ -1,6 +1,5 @@
use std::path::PathBuf;
use smithay::reexports::calloop;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
+6 -1
View File
@@ -1,9 +1,9 @@
use smithay::reexports::calloop;
use zbus::blocking::Connection;
use zbus::Interface;
use crate::niri::State;
pub mod freedesktop_screensaver;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
@@ -13,6 +13,7 @@ pub mod mutter_screen_cast;
#[cfg(feature = "xdp-gnome-screencast")]
use mutter_screen_cast::ScreenCast;
use self::freedesktop_screensaver::ScreenSaver;
use self::mutter_display_config::DisplayConfig;
use self::mutter_service_channel::ServiceChannel;
@@ -24,6 +25,7 @@ trait Start: Interface {
pub struct DBusServers {
pub conn_service_channel: Option<Connection>,
pub conn_display_config: Option<Connection>,
pub conn_screen_saver: Option<Connection>,
pub conn_screen_shot: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
@@ -48,6 +50,9 @@ impl DBusServers {
let display_config = DisplayConfig::new(backend.enabled_outputs());
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
dbus.conn_screen_saver = try_start(screen_saver);
let (to_niri, from_screenshot) = calloop::channel::channel();
let (to_screenshot, from_niri) = async_channel::unbounded();
niri.event_loop
+3 -2
View File
@@ -5,7 +5,7 @@ use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
@@ -112,7 +112,8 @@ impl DisplayConfig {
Ok((0, monitors, logical_monitors, HashMap::new()))
}
// FIXME: monitors-changed signal.
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
+15 -2
View File
@@ -5,12 +5,12 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::utils::output_size;
#[derive(Clone)]
pub struct ScreenCast {
@@ -54,6 +54,13 @@ pub struct Stream {
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
/// Size of the stream in logical coordinates.
size: (i32, i32),
}
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
@@ -195,6 +202,12 @@ impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let size = output_size(&self.output).into();
StreamParameters { size }
}
}
impl ScreenCast {
+77 -10
View File
@@ -16,9 +16,9 @@ use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell;
use crate::niri::{ClientState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -75,7 +75,7 @@ impl CompositorHandler for State {
}
}
}
})
});
}
fn commit(&mut self, surface: &WlSurface) {
@@ -97,23 +97,81 @@ impl CompositorHandler for State {
// 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());
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 {
// The toplevel got mapped.
let window = entry.remove();
let Unmapped { window, state } = entry.remove();
window.on_commit();
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
{
let (width, is_full_width, output) =
if let InitialConfigureState::Configured {
width,
is_full_width,
output,
..
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(width, is_full_width, output)
} else {
error!("window map must happen after initial configure");
(None, false, None)
};
let parent = window
.toplevel()
.expect("no x11 support")
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
// Only consider the parent if we configured the window for the same
// output.
//
// Normally when we're following the parent, the configured output will be
// 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)
})
.map(|(window, _)| window.clone());
let win = 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, win, width, is_full_width)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, win, width, is_full_width);
Some(output)
} else {
self.niri.layout.add_window(win, width, is_full_width)
};
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
self.niri.queue_redraw(output);
}
return;
}
// The toplevel remains unmapped.
let window = entry.get();
xdg_shell::send_initial_configure_if_needed(window.toplevel());
let unmapped = entry.get();
if unmapped.needs_initial_configure() {
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
self.queue_initial_configure(toplevel);
}
return;
}
@@ -125,12 +183,21 @@ impl CompositorHandler for State {
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
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 {
// The toplevel got unmapped.
self.niri.layout.remove_window(&window);
self.niri.unmapped_windows.insert(surface.clone(), window);
// Newly-unmapped toplevels must perform the initial commit-configure sequence
// afresh.
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(output);
return;
}
+176 -13
View File
@@ -9,10 +9,12 @@ use std::sync::Arc;
use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
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;
@@ -20,7 +22,13 @@ use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
@@ -39,18 +47,25 @@ use smithay::wayland::session_lock::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
};
use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
use crate::utils::output_size;
use crate::{delegate_foreign_toplevel, delegate_screencopy};
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<State> {
&mut self.niri.seat_state
@@ -73,6 +88,19 @@ impl SeatHandler for State {
set_data_device_focus(dh, seat, client.clone());
set_primary_focus(dh, seat, client);
}
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
let keyboards = self
.niri
.devices
.iter()
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
.cloned();
for mut keyboard in keyboards {
keyboard.led_update(led_state.into());
}
}
}
delegate_seat!(State);
delegate_cursor_shape!(State);
@@ -189,6 +217,11 @@ impl DataControlHandler for State {
delegate_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);
}
}
delegate_output!(State);
delegate_presentation!(State);
@@ -204,13 +237,10 @@ impl DmabufHandler for State {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
match self.backend.import_dmabuf(&dmabuf) {
Ok(_) => {
let _ = notifier.successful::<State>();
}
Err(_) => {
notifier.failed();
}
if self.backend.import_dmabuf(&dmabuf) {
let _ = notifier.successful::<State>();
} else {
notifier.failed();
}
}
}
@@ -268,7 +298,7 @@ impl SecurityContextHandler for State {
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
error!("error inserting client: {err}");
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
@@ -277,3 +307,136 @@ impl SecurityContextHandler for State {
}
}
delegate_security_context!(State);
impl IdleNotifierHandler for State {
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
&mut self.niri.idle_notifier_state
}
}
delegate_idle_notify!(State);
impl IdleInhibitHandler for State {
fn inhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.insert(surface);
}
fn uninhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.remove(&surface);
}
}
delegate_idle_inhibit!(State);
impl ForeignToplevelHandler for State {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
&mut self.niri.foreign_toplevel_state
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.activate_window(&window);
self.niri.queue_redraw_all();
}
}
fn close(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
window.toplevel().expect("no x11 support").send_close();
}
}
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !window
.toplevel()
.expect("no x11 support")
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
let window = 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.clone(), &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
}
delegate_foreign_toplevel!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, screencopy: Screencopy) {
if let Err(err) = self
.niri
.render_for_screencopy(&mut self.backend, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
}
}
}
delegate_screencopy!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
&mut self
.backend
.tty()
.get_device_from_node(node)
.unwrap()
.drm_lease_state
}
fn lease_request(
&mut self,
node: DrmNode,
request: DrmLeaseRequest,
) -> Result<DrmLeaseBuilder, LeaseRejected> {
debug!(
"Received lease request for {} connectors",
request.connectors.len()
);
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.lease_request(request)
}
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
debug!("Lease success");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.new_lease(lease);
}
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
debug!("Destroyed lease");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.remove_lease(lease_id);
}
}
delegate_drm_lease!(State);
delegate_viewporter!(State);
+400 -57
View File
@@ -1,3 +1,4 @@
use niri_config::{Match, WindowRule};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -13,17 +14,99 @@ use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}
pub fn resolve_window_rules(
rules: &[WindowRule],
toplevel: &ToplevelSurface,
) -> ResolvedWindowRules {
let _span = tracy_client::span!("resolve_window_rules");
let mut resolved = ResolvedWindowRules::default();
with_states(toplevel.wl_surface(), |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let mut open_on_output = None;
for rule in rules {
if !(rule.matches.is_empty() || rule.matches.iter().any(|m| window_matches(&role, m))) {
continue;
}
if rule.excludes.iter().any(|m| window_matches(&role, m)) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -32,27 +115,8 @@ impl XdgShellHandler for State {
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface().clone();
let window = Window::new(surface);
// Tell the surface the preferred size and bounds for its likely output.
if let Some(ws) = self.niri.layout.active_workspace() {
ws.configure_new_window(&window);
}
// 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.
let config = self.niri.config.borrow();
if config.prefer_no_csd {
window.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);
});
}
// At the moment of creation, xdg toplevels must have no buffer.
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
let unmapped = Unmapped::new(Window::new_wayland_window(surface));
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
assert!(existing.is_none());
}
@@ -94,6 +158,15 @@ impl XdgShellHandler for State {
}
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
@@ -140,7 +213,9 @@ impl XdgShellHandler for State {
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
if Some(&root)
!= layout_focus.map(|win| win.toplevel().expect("no x11 support").wl_surface())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -185,9 +260,11 @@ impl XdgShellHandler for State {
fn maximize_request(&mut self, surface: ToplevelSurface) {
// FIXME
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
// 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) {
surface.send_configure();
}
}
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
@@ -196,46 +273,167 @@ impl XdgShellHandler for State {
fn fullscreen_request(
&mut self,
surface: ToplevelSurface,
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
if surface
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
let window = 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.clone(), &requested_output);
}
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
}
self.niri.layout.set_fullscreen(&window, true);
}
}
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
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 } => {
*wants_fullscreen = Some(requested_output);
// The required configure will be the initial configure.
}
InitialConfigureState::Configured { output, .. } => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = requested_output
.as_ref()
// If none requested, try currently configured output.
.or(output.as_ref())
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in fullscreen_request()");
toplevel.send_configure();
}
}
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((window, _)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
.find_window_and_output(toplevel.wl_surface())
{
let window = 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();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = None;
// The required configure will be the initial configure.
}
InitialConfigureState::Configured {
width,
is_full_width,
output,
..
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in unfullscreen_request()");
toplevel.send_configure();
}
}
@@ -271,6 +469,14 @@ impl XdgShellHandler for State {
self.niri.queue_redraw(output.clone());
}
}
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
}
delegate_xdg_shell!(State);
@@ -323,14 +529,14 @@ impl KdeDecorationHandler for State {
&self.niri.kde_decoration_state
}
}
delegate_kde_decoration!(State);
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
if !initial_configure_sent(toplevel) {
toplevel.send_configure();
impl XdgForeignHandler for State {
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
&mut self.niri.xdg_foreign_state
}
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
@@ -345,6 +551,131 @@ fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
error!("window must be present in unmapped_windows in send_initial_configure()");
return;
};
let Unmapped { window, state } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
return;
};
let config = self.niri.config.borrow();
let rules = resolve_window_rules(&config.window_rules, toplevel);
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o));
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
wants_fullscreen
.as_ref()
.and_then(|x| x.as_ref())
// The monitor might not exist if the output was disconnected.
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if this is a dialog with a parent, to place it next to the parent.
let mon = mon.map(|mon| (mon, false)).or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
// If not, use the active monitor.
let mon = mon.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
// If we're following the parent, don't set the target output, so that when the window is
// 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());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|| rules.open_fullscreen == Some(true)
{
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
width = ws.resolve_default_width(rules.default_width);
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width);
}
// 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 configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
};
toplevel.send_configure();
}
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
// Send the initial configure in an idle, in case the client sent some more info after the
// initial commit.
self.niri.event_loop.insert_idle(move |state| {
if !toplevel.alive() {
return;
}
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
if unmapped.needs_initial_configure() {
state.send_initial_configure(&toplevel);
}
}
});
}
/// Should be called on `WlSurface::commit`
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
self.niri.popups.commit(surface);
@@ -451,7 +782,9 @@ impl State {
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
for (popup, _) in PopupManager::popups_for_surface(
window.toplevel().expect("no x11 support").wl_surface(),
) {
match popup {
PopupKind::Xdg(ref popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
@@ -465,6 +798,16 @@ impl State {
}
}
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
let resolve = || resolve_window_rules(&self.niri.config.borrow().window_rules, toplevel);
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = resolve();
}
}
}
}
fn unconstrain_with_padding(
+509 -49
View File
@@ -1,13 +1,16 @@
use std::any::Any;
use std::collections::HashSet;
use std::time::Duration;
use niri_config::{Action, Binds, LayoutAction, Modifiers};
use input::event::gesture::GestureEventCoordinates as _;
use niri_config::{Action, Binds, Modifiers};
use niri_ipc::LayoutSwitchTarget;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent,
PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState,
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
};
use smithay::backend::libinput::LibinputInputBackend;
use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState};
@@ -16,14 +19,15 @@ use smithay::input::pointer::{
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, RelativeMotionEvent,
};
use smithay::reexports::input;
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use crate::niri::State;
use crate::screenshot_ui::ScreenshotUi;
use crate::utils::{center, get_monotonic_time, spawn};
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
use crate::utils::{center, get_monotonic_time};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompositorMod {
@@ -37,7 +41,7 @@ pub struct TabletData {
}
impl State {
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>)
pub fn process_input_event<I: InputBackend + 'static>(&mut self, event: InputEvent<I>)
where
I::Device: 'static, // Needed for downcasting.
{
@@ -49,9 +53,24 @@ impl State {
// here.
self.niri.layout.advance_animations(get_monotonic_time());
// Power on monitors if they were off.
if should_activate_monitors(&event) {
self.niri.activate_monitors(&self.backend);
if self.niri.monitors_active {
// Notify the idle-notifier of activity.
if should_notify_activity(&event) {
self.niri
.idle_notifier_state
.notify_activity(&self.niri.seat);
}
} else {
// Power on monitors if they were off.
if should_activate_monitors(&event) {
self.niri.activate_monitors(&mut self.backend);
// Notify the idle-notifier of activity only if we're also powering on the
// monitors.
self.niri
.idle_notifier_state
.notify_activity(&self.niri.seat);
}
}
let hide_hotkey_overlay =
@@ -85,11 +104,12 @@ impl State {
GesturePinchEnd { event } => self.on_gesture_pinch_end::<I>(event),
GestureHoldBegin { event } => self.on_gesture_hold_begin::<I>(event),
GestureHoldEnd { event } => self.on_gesture_hold_end::<I>(event),
TouchDown { .. } => (),
TouchMotion { .. } => (),
TouchUp { .. } => (),
TouchCancel { .. } => (),
TouchFrame { .. } => (),
TouchDown { event } => self.on_touch_down::<I>(event),
TouchMotion { event } => self.on_touch_motion::<I>(event),
TouchUp { event } => self.on_touch_up::<I>(event),
TouchCancel { event } => self.on_touch_cancel::<I>(event),
TouchFrame { event } => self.on_touch_frame::<I>(event),
SwitchToggle { .. } => (),
Special(_) => (),
}
@@ -126,9 +146,25 @@ impl State {
}
}
if device.has_capability(input::DeviceCapability::Keyboard) {
if let Some(led_state) = self
.niri
.seat
.get_keyboard()
.map(|keyboard| keyboard.led_state())
{
device.led_update(led_state.into());
}
}
if device.has_capability(input::DeviceCapability::Touch) {
self.niri.touch.insert(device.clone());
}
apply_libinput_settings(&self.niri.config.borrow().input, device);
}
InputEvent::DeviceRemoved { device } => {
self.niri.touch.remove(device);
self.niri.tablets.remove(device);
self.niri.devices.remove(device);
}
@@ -143,6 +179,9 @@ impl State {
let desc = TabletDescriptor::from(&device);
tablet_seat.add_tablet::<Self>(&self.niri.display_handle, &desc);
}
if device.has_capability(DeviceCapability::Touch) && self.niri.seat.get_touch().is_none() {
self.niri.seat.add_touch();
}
}
fn on_device_removed(&mut self, device: impl Device) {
@@ -157,6 +196,9 @@ impl State {
tablet_seat.clear_tools();
}
}
if device.has_capability(DeviceCapability::Touch) && self.niri.touch.is_empty() {
self.niri.seat.remove_touch();
}
}
/// Computes the cursor position for the tablet event.
@@ -220,6 +262,7 @@ impl State {
if let Some(dialog) = &this.niri.exit_confirm_dialog {
if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
info!("quitting after confirming exit dialog");
this.niri.stop_signal.stop();
}
}
@@ -246,24 +289,35 @@ impl State {
return;
}
self.do_action(action);
}
pub fn do_action(&mut self, action: Action) {
if self.niri.is_locked() && !allowed_when_locked(&action) {
return;
}
if let Some(touch) = self.niri.seat.get_touch() {
touch.cancel(self);
}
match action {
Action::Quit => {
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
if dialog.show() {
self.niri.queue_redraw_all();
Action::Quit(skip_confirmation) => {
if !skip_confirmation {
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
if dialog.show() {
self.niri.queue_redraw_all();
}
return;
}
} else {
info!("quitting because quit bind was pressed");
self.niri.stop_signal.stop()
}
info!("quitting as requested");
self.niri.stop_signal.stop()
}
Action::ChangeVt(vt) => {
self.backend.change_vt(vt);
// Changing `VT` may not deliver the key releases, so clear the state.
// Changing VT may not deliver the key releases, so clear the state.
self.niri.suppressed_keys.clear();
}
Action::Suspend => {
@@ -272,7 +326,7 @@ impl State {
self.niri.suppressed_keys.clear();
}
Action::PowerOffMonitors => {
self.niri.deactivate_monitors(&self.backend);
self.niri.deactivate_monitors(&mut self.backend);
}
Action::ToggleDebugTint => {
self.backend.toggle_debug_tint();
@@ -335,7 +389,7 @@ impl State {
}
Action::CloseWindow => {
if let Some(window) = self.niri.layout.focus() {
window.toplevel().send_close();
window.toplevel().expect("no x11 support").send_close();
}
}
Action::FullscreenWindow => {
@@ -350,8 +404,8 @@ impl State {
self.niri.seat.get_keyboard().unwrap().with_xkb_state(
self,
|mut state| match action {
LayoutAction::Next => state.cycle_next_layout(),
LayoutAction::Prev => state.cycle_prev_layout(),
LayoutSwitchTarget::Next => state.cycle_next_layout(),
LayoutSwitchTarget::Prev => state.cycle_prev_layout(),
},
);
}
@@ -395,6 +449,16 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowLeft => {
self.niri.layout.consume_or_expel_window_left();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowRight => {
self.niri.layout.consume_or_expel_window_right();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
// FIXME: granular
@@ -541,48 +605,56 @@ impl State {
Action::MoveWindowToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWindowToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWindowToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWindowToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
}
}
@@ -597,6 +669,30 @@ impl State {
self.niri.queue_redraw_all();
}
}
Action::MoveWorkspaceToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
}
}
@@ -1091,13 +1187,7 @@ impl State {
fn on_gesture_swipe_begin<I: InputBackend>(&mut self, event: I::GestureSwipeBeginEvent) {
if event.fingers() == 3 {
if let Some(output) = self.niri.output_under_cursor() {
self.niri.layout.workspace_switch_gesture_begin(&output);
// FIXME: granular. This one is awkward because this can cancel a gesture on
// multiple other outputs in theory.
self.niri.queue_redraw_all();
}
self.niri.gesture_swipe_3f_cumulative = Some((0., 0.));
// We handled this event.
return;
@@ -1120,16 +1210,75 @@ impl State {
);
}
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
fn on_gesture_swipe_update<I: InputBackend + 'static>(
&mut self,
event: I::GestureSwipeUpdateEvent,
) where
I::Device: 'static,
{
let mut delta_x = event.delta_x();
let mut delta_y = event.delta_y();
if let Some(libinput_event) =
(&event as &dyn Any).downcast_ref::<input::event::gesture::GestureSwipeUpdateEvent>()
{
delta_x = libinput_event.dx_unaccelerated();
delta_y = libinput_event.dy_unaccelerated();
}
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
delta_x = -delta_x;
delta_y = -delta_y;
}
}
if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
*cx += delta_x;
*cy += delta_y;
// Check if the gesture moved far enough to decide. Threshold copied from GNOME Shell.
let (cx, cy) = (*cx, *cy);
if cx * cx + cy * cy >= 16. * 16. {
self.niri.gesture_swipe_3f_cumulative = None;
if let Some(output) = self.niri.output_under_cursor() {
if cx.abs() > cy.abs() {
self.niri.layout.view_offset_gesture_begin(&output);
} else {
self.niri.layout.workspace_switch_gesture_begin(&output);
}
}
}
}
let timestamp = Duration::from_micros(event.time());
let mut handled = false;
let res = self
.niri
.layout
.workspace_switch_gesture_update(event.delta_y());
.workspace_switch_gesture_update(delta_y, timestamp);
if let Some(output) = res {
if let Some(output) = output {
self.niri.queue_redraw(output);
}
handled = true;
}
let res = self
.niri
.layout
.view_offset_gesture_update(delta_x, timestamp);
if let Some(output) = res {
if let Some(output) = output {
self.niri.queue_redraw(output);
}
handled = true;
}
if handled {
// We handled this event.
return;
}
@@ -1150,13 +1299,25 @@ impl State {
}
fn on_gesture_swipe_end<I: InputBackend>(&mut self, event: I::GestureSwipeEndEvent) {
self.niri.gesture_swipe_3f_cumulative = None;
let mut handled = false;
let res = self
.niri
.layout
.workspace_switch_gesture_end(event.cancelled());
if let Some(output) = res {
self.niri.queue_redraw(output);
handled = true;
}
let res = self.niri.layout.view_offset_gesture_end(event.cancelled());
if let Some(output) = res {
self.niri.queue_redraw(output);
handled = true;
}
if handled {
// We handled this event.
return;
}
@@ -1267,6 +1428,116 @@ impl State {
},
);
}
/// Computes the cursor position for the touch event.
///
/// This function handles the touch output mapping, as well as coordinate transform
fn compute_touch_location<I: InputBackend, E: AbsolutePositionEvent<I>>(
&self,
evt: &E,
) -> Option<Point<f64, Logical>> {
let output = self.niri.output_for_touch()?;
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let transform = output.current_transform();
let size = transform.invert().transform_size(output_geo.size);
Some(
transform.transform_point_in(evt.position_transformed(size), &size.to_f64())
+ output_geo.loc.to_f64(),
)
}
fn on_touch_down<I: InputBackend>(&mut self, evt: I::TouchDownEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
return;
};
let Some(touch_location) = self.compute_touch_location(&evt) else {
return;
};
if !handle.is_grabbed() {
let output_under_touch = self
.niri
.global_space
.output_under(touch_location)
.next()
.cloned();
if let Some(window) = self.niri.window_under(touch_location) {
let window = window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = output_under_touch {
self.niri.layout.activate_output(&output);
// FIXME: granular.
self.niri.queue_redraw_all();
};
};
let serial = SERIAL_COUNTER.next_serial();
let under = self
.niri
.surface_under_and_global_space(touch_location)
.map(|under| under.surface);
handle.down(
self,
under,
&DownEvent {
slot: evt.slot(),
location: touch_location,
serial,
time: evt.time_msec(),
},
);
}
fn on_touch_up<I: InputBackend>(&mut self, evt: I::TouchUpEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
return;
};
let serial = SERIAL_COUNTER.next_serial();
handle.up(
self,
&UpEvent {
slot: evt.slot(),
serial,
time: evt.time_msec(),
},
)
}
fn on_touch_motion<I: InputBackend>(&mut self, evt: I::TouchMotionEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
return;
};
let Some(touch_location) = self.compute_touch_location(&evt) else {
return;
};
let under = self
.niri
.surface_under_and_global_space(touch_location)
.map(|under| under.surface);
handle.motion(
self,
under,
&TouchMotionEvent {
slot: evt.slot(),
location: touch_location,
time: evt.time_msec(),
},
);
}
fn on_touch_frame<I: InputBackend>(&mut self, _evt: I::TouchFrameEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
return;
};
handle.frame(self);
}
fn on_touch_cancel<I: InputBackend>(&mut self, _evt: I::TouchCancelEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
return;
};
handle.cancel(self);
}
}
/// Check whether the key should be intercepted and mark intercepted
@@ -1351,6 +1622,15 @@ fn action(
_ => (),
}
bound_action(bindings, comp_mod, raw, mods)
}
fn bound_action(
bindings: &Binds,
comp_mod: CompositorMod,
raw: Option<Keysym>,
mods: ModifiersState,
) -> Option<Action> {
// Handle configured binds.
let mut modifiers = Modifiers::empty();
if mods.ctrl {
@@ -1366,14 +1646,12 @@ fn action(
modifiers |= Modifiers::SUPER;
}
let (mod_down, mut comp_mod) = match comp_mod {
let (mod_down, comp_mod) = match comp_mod {
CompositorMod::Super => (mods.logo, Modifiers::SUPER),
CompositorMod::Alt => (mods.alt, Modifiers::ALT),
};
if mod_down {
modifiers |= Modifiers::COMPOSITOR;
} else {
comp_mod = Modifiers::empty();
}
let raw = raw?;
@@ -1383,8 +1661,15 @@ fn action(
continue;
}
if bind.key.modifiers | comp_mod == modifiers {
return bind.actions.first().cloned();
let mut bind_modifiers = bind.key.modifiers;
if bind_modifiers.contains(Modifiers::COMPOSITOR) {
bind_modifiers |= comp_mod;
} else if bind_modifiers.contains(comp_mod) {
bind_modifiers |= Modifiers::COMPOSITOR;
}
if bind_modifiers == modifiers {
return Some(bind.action.clone());
}
}
@@ -1394,9 +1679,9 @@ fn action(
fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
match event {
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
InputEvent::PointerMotion { .. }
| InputEvent::PointerMotionAbsolute { .. }
| InputEvent::PointerButton { .. }
| InputEvent::PointerAxis { .. }
| InputEvent::GestureSwipeBegin { .. }
| InputEvent::GesturePinchBegin { .. }
@@ -1415,8 +1700,8 @@ fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
match event {
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
InputEvent::PointerButton { .. }
| InputEvent::PointerAxis { .. }
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
InputEvent::PointerAxis { .. }
| InputEvent::GestureSwipeBegin { .. }
| InputEvent::GesturePinchBegin { .. }
| InputEvent::TouchDown { .. }
@@ -1430,8 +1715,8 @@ fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bool {
match event {
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
InputEvent::PointerButton { .. }
| InputEvent::PointerAxis { .. }
InputEvent::PointerButton { event } if event.state() == ButtonState::Pressed => true,
InputEvent::PointerAxis { .. }
| InputEvent::GestureSwipeBegin { .. }
| InputEvent::GesturePinchBegin { .. }
| InputEvent::TouchDown { .. }
@@ -1442,10 +1727,17 @@ fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bo
}
}
fn should_notify_activity<I: InputBackend>(event: &InputEvent<I>) -> bool {
!matches!(
event,
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. }
)
}
fn allowed_when_locked(action: &Action) -> bool {
matches!(
action,
Action::Quit
Action::Quit(_)
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
@@ -1456,7 +1748,7 @@ fn allowed_when_locked(action: &Action) -> bool {
fn allowed_during_screenshot(action: &Action) -> bool {
matches!(
action,
Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
Action::Quit(_) | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
)
}
@@ -1467,6 +1759,7 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
let c = &config.touchpad;
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_dwt_set_enabled(c.dwt);
let _ = device.config_dwtp_set_enabled(c.dwtp);
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
@@ -1513,11 +1806,23 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
let _ = device.config_accel_set_profile(default);
}
}
if is_trackpoint {
let c = &config.trackpoint;
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
if let Some(accel_profile) = c.accel_profile {
let _ = device.config_accel_set_profile(accel_profile.into());
} else if let Some(default) = device.config_accel_default_profile() {
let _ = device.config_accel_set_profile(default);
}
}
}
#[cfg(test)]
mod tests {
use niri_config::{Action, Bind, Binds, Key, Modifiers};
use niri_config::{Bind, Key};
use super::*;
@@ -1529,7 +1834,7 @@ mod tests {
keysym: close_keysym,
modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL,
},
actions: vec![Action::CloseWindow],
action: Action::CloseWindow,
}]);
let comp_mod = CompositorMod::Super;
@@ -1642,4 +1947,159 @@ mod tests {
// Ensure that no keys are being suppressed.
assert!(suppressed_keys.is_empty());
}
#[test]
fn comp_mod_handling() {
let bindings = Binds(vec![
Bind {
key: Key {
keysym: Keysym::q,
modifiers: Modifiers::COMPOSITOR,
},
action: Action::CloseWindow,
},
Bind {
key: Key {
keysym: Keysym::h,
modifiers: Modifiers::SUPER,
},
action: Action::FocusColumnLeft,
},
Bind {
key: Key {
keysym: Keysym::j,
modifiers: Modifiers::empty(),
},
action: Action::FocusWindowDown,
},
Bind {
key: Key {
keysym: Keysym::k,
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
},
action: Action::FocusWindowUp,
},
Bind {
key: Key {
keysym: Keysym::l,
modifiers: Modifiers::SUPER | Modifiers::ALT,
},
action: Action::FocusColumnRight,
},
]);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::q),
ModifiersState {
logo: true,
..Default::default()
}
),
Some(Action::CloseWindow)
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::q),
ModifiersState::default(),
),
None,
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::h),
ModifiersState {
logo: true,
..Default::default()
}
),
Some(Action::FocusColumnLeft)
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::h),
ModifiersState::default(),
),
None,
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::j),
ModifiersState {
logo: true,
..Default::default()
}
),
None,
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::j),
ModifiersState::default(),
),
Some(Action::FocusWindowDown)
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::k),
ModifiersState {
logo: true,
..Default::default()
}
),
Some(Action::FocusWindowUp)
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::k),
ModifiersState::default(),
),
None,
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::l),
ModifiersState {
logo: true,
alt: true,
..Default::default()
}
),
Some(Action::FocusColumnRight)
);
assert_eq!(
bound_action(
&bindings,
CompositorMod::Super,
Some(Keysym::l),
ModifiersState {
logo: true,
..Default::default()
},
),
None,
);
}
}
+17 -8
View File
@@ -3,10 +3,10 @@ use std::io::{Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use anyhow::{bail, Context};
use niri_ipc::{Mode, Output, Request, Response};
use anyhow::{anyhow, bail, Context};
use niri_ipc::{Mode, Output, Reply, Request, Response};
use crate::Msg;
use crate::cli::Msg;
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
@@ -19,8 +19,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match msg {
let request = match &msg {
Msg::Outputs => Request::Outputs,
Msg::Action { action } => Request::Action(action.clone()),
};
let mut buf = serde_json::to_vec(&request).unwrap();
stream
@@ -35,12 +36,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
.read_to_end(&mut buf)
.context("error reading IPC response")?;
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
let response = reply
.map_err(|msg| anyhow!(msg))
.context("niri could not handle the request")?;
match msg {
Msg::Outputs => {
#[allow(irrefutable_let_patterns)]
let Response::Outputs(outputs) = response
else {
let Response::Outputs(outputs) = response else {
bail!("unexpected response: expected Outputs, got {response:?}");
};
@@ -100,6 +104,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!();
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
}
Ok(())
+23 -8
View File
@@ -22,6 +22,7 @@ pub struct IpcServer {
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
}
@@ -85,6 +86,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
};
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
ipc_outputs: state.backend.ipc_outputs(),
};
@@ -108,20 +110,33 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
.await
.context("error reading request")?;
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
let reply = process(&ctx, &buf).map_err(|err| {
warn!("error processing IPC request: {err:?}");
err.to_string()
});
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
write.write_all(&buf).await.context("error writing reply")?;
Ok(())
}
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
let response = match request {
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
Response::Outputs(ipc_outputs)
}
Request::Action(action) => {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action);
});
Response::Handled
}
};
let buf = serde_json::to_vec(&response).context("error formatting response")?;
write
.write_all(&buf)
.await
.context("error writing response")?;
Ok(())
Ok(response)
}
+99 -50
View File
@@ -1,64 +1,72 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{self, Color};
use niri_config::GradientRelativeTo;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use crate::niri_render_elements;
use crate::render_helpers::gradient::GradientRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
is_off: bool,
sizes: [Size<i32, Logical>; 4],
full_size: Size<i32, Logical>,
is_active: bool,
is_border: bool,
width: i32,
active_color: Color,
inactive_color: Color,
config: niri_config::FocusRing,
}
pub type FocusRingRenderElement = SolidColorRenderElement;
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = GradientRenderElement,
}
}
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
is_off: config.off,
sizes: Default::default(),
full_size: Default::default(),
is_active: false,
is_border: false,
width: config.width.into(),
active_color: config.active_color,
inactive_color: config.inactive_color,
config,
}
}
pub fn update_config(&mut self, config: niri_config::FocusRing) {
self.is_off = config.off;
self.width = config.width.into();
self.active_color = config.active_color;
self.inactive_color = config.inactive_color;
self.config = config;
}
pub fn update(
&mut self,
win_pos: Point<i32, Logical>,
win_size: Size<i32, Logical>,
is_border: bool,
) {
if is_border {
self.buffers[0].resize((win_size.w + self.width * 2, self.width));
self.buffers[1].resize((win_size.w + self.width * 2, self.width));
self.buffers[2].resize((self.width, win_size.h));
self.buffers[3].resize((self.width, win_size.h));
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
self.locations[0] = win_pos + Point::from((-self.width, -self.width));
self.locations[1] = win_pos + Point::from((-self.width, win_size.h));
self.locations[2] = win_pos + Point::from((-self.width, 0));
self.locations[3] = win_pos + Point::from((win_size.w, 0));
if is_border {
self.sizes[0] = Size::from((win_size.w + width * 2, width));
self.sizes[1] = Size::from((win_size.w + width * 2, width));
self.sizes[2] = Size::from((width, win_size.h));
self.sizes[3] = Size::from((width, win_size.h));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
let size = win_size + Size::from((self.width * 2, self.width * 2));
self.buffers[0].resize(size);
self.locations[0] = win_pos - Point::from((self.width, self.width));
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
}
self.is_border = is_border;
@@ -66,50 +74,91 @@ impl FocusRing {
pub fn set_active(&mut self, is_active: bool) {
let color = if is_active {
self.active_color.into()
self.config.active_color.into()
} else {
self.inactive_color.into()
self.config.inactive_color.into()
};
for buf in &mut self.buffers {
buf.set_color(color);
}
self.is_active = is_active;
}
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.is_off {
if self.config.off {
return rv.into_iter();
}
let mut push = |buffer, location: Point<i32, Logical>| {
let elem = SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
);
let gradient = if self.is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
let elem = gradient.and_then(|gradient| {
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
GradientRenderElement::new(
renderer,
scale,
Rectangle::from_loc_and_size(location, size),
gradient_area,
gradient.from.into(),
gradient.to.into(),
((gradient.angle as f32) - 90.).to_radians(),
)
.map(Into::into)
});
let elem = elem.unwrap_or_else(|| {
SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
rv.push(elem);
};
if self.is_border {
for (buf, loc) in zip(&self.buffers, self.locations) {
push(buf, loc);
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
push(buf, location + loc, size);
}
} else {
push(&self.buffers[0], self.locations[0]);
push(
&self.buffers[0],
location + self.locations[0],
self.sizes[0],
);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.width
self.config.width.into()
}
pub fn is_off(&self) -> bool {
self.is_off
self.config.off
}
}
+690 -121
View File
File diff suppressed because it is too large Load Diff
+195 -33
View File
@@ -2,12 +2,10 @@ use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_config::SizeChange;
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
@@ -16,8 +14,19 @@ use super::workspace::{
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::render_helpers::renderer::NiriRenderer;
use crate::rubber_band::RubberBand;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::output_size;
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
stiffness: 0.5,
limit: 0.05,
};
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
@@ -44,6 +53,7 @@ pub struct WorkspaceSwitchGesture {
pub center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
pub tracker: SwipeTracker,
}
pub type MonitorRenderElement<R> =
@@ -77,6 +87,10 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn active_workspace_ref(&self) -> &Workspace<W> {
&self.workspaces[self.active_workspace_idx]
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
@@ -86,6 +100,7 @@ impl<W: LayoutElement> Monitor<W> {
return;
}
// FIXME: also compute and use current velocity.
let current_idx = self
.workspace_switch
.as_ref()
@@ -97,7 +112,9 @@ impl<W: LayoutElement> Monitor<W> {
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
current_idx,
idx as f64,
Duration::from_millis(250),
0.,
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
}
@@ -127,6 +144,26 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace_idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(right_of))
.unwrap();
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window_right_of(right_of, window, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
@@ -216,6 +253,14 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn consume_or_expel_window_left(&mut self) {
self.active_workspace().consume_or_expel_window_left();
}
pub fn consume_or_expel_window_right(&mut self) {
self.active_workspace().consume_or_expel_window_right();
}
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
}
@@ -547,14 +592,28 @@ impl<W: LayoutElement> Monitor<W> {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor() as usize;
let after_idx = render_idx.ceil() as usize;
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
(before_idx, Point::from((0, offset)))
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0, offset)))
} else {
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0, -size.h + offset)))
};
@@ -578,16 +637,11 @@ impl<W: LayoutElement> Monitor<W> {
let ws = &self.workspaces[self.active_workspace_idx];
ws.render_above_top_layer()
}
}
impl Monitor<Window> {
pub fn render_elements<R: Renderer + ImportAll>(
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> Vec<MonitorRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
@@ -598,37 +652,63 @@ impl Monitor<Window> {
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor() as usize;
let after_idx = render_idx.ceil() as usize;
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return vec![];
}
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
let after = self.workspaces[after_idx].render_elements(renderer);
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: crop to infinite bounds for all sides except the side
// where the workspaces join,
// otherwise it will cut pixel shaders and mess up
// the coordinate space.
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
if before_idx < 0. {
return after.collect();
}
Some(after)
} else {
None
};
let before_idx = before_idx as usize;
let before = self.workspaces[before_idx].render_elements(renderer);
let after = self.workspaces[after_idx].render_elements(renderer);
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities((0, offset), (size.w, size.h)),
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, size.h),
),
)?,
(0, -offset),
Relocate::Relative,
))
});
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities((0, 0), (size.w, offset)),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
before.chain(after).collect()
before.chain(after.into_iter().flatten()).collect()
}
None => {
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
@@ -656,4 +736,86 @@ impl Monitor<Window> {
}
}
}
pub fn workspace_switch_gesture_begin(&mut self) {
let center_idx = self.active_workspace_idx;
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(center_idx as f64);
let gesture = WorkspaceSwitchGesture {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn workspace_switch_gesture_update(
&mut self,
delta_y: f64,
timestamp: Duration,
) -> Option<bool> {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None;
};
gesture.tracker.push(delta_y, timestamp);
let pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
if gesture.current_idx == new_idx {
return Some(false);
}
gesture.current_idx = new_idx;
Some(true)
}
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
if cancelled {
self.workspace_switch = None;
self.clean_up_workspaces();
return true;
}
let mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
let new_idx = new_idx.round() as usize;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
min,
max,
gesture.center_idx as f64 + current_pos,
);
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
gesture.current_idx,
new_idx as f64,
velocity,
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
true
}
}
+152 -43
View File
@@ -3,14 +3,16 @@ use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::FocusRing;
use super::workspace::WorkspaceRenderElement;
use super::{LayoutElement, Options};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::{LayoutElement, LayoutElementRenderElement, Options};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -21,6 +23,12 @@ pub struct Tile<W: LayoutElement> {
/// The border around the window.
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,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
@@ -33,24 +41,39 @@ pub struct Tile<W: LayoutElement> {
/// The size we were requested to fullscreen into.
fullscreen_size: Size<i32, Logical>,
/// The animation upon opening a window.
open_animation: Option<Animation>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
niri_render_elements! {
TileRenderElement<R> => {
LayoutElement = LayoutElementRenderElement<R>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
}
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
Self {
window,
border: FocusRing::new(options.border),
border: FocusRing::new(options.border.into()),
focus_ring: FocusRing::new(options.focus_ring),
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(),
open_animation: None,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
self.border.update_config(options.border);
self.border.update_config(options.border.into());
self.focus_ring.update_config(options.focus_ring);
self.options = options;
}
@@ -61,14 +84,37 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
let width = self.border.width();
self.border.update(
(width, width).into(),
self.window.size(),
self.window.has_ssd(),
);
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
self.border
.update(self.window.size(), self.window.has_ssd());
self.border.set_active(is_active);
self.focus_ring.update(self.tile_size(), self.has_ssd());
self.focus_ring.set_active(is_active);
match &mut self.open_animation {
Some(anim) => {
anim.set_current_time(current_time);
if anim.is_done() {
self.open_animation = None;
}
}
None => (),
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_animation.is_some()
}
pub fn start_open_animation(&mut self) {
self.open_animation = Some(Animation::new(
0.,
1.,
0.,
self.options.animations.window_open,
niri_config::Animation::default_window_open(),
));
}
pub fn window(&self) -> &W {
@@ -141,6 +187,23 @@ impl<W: LayoutElement> Tile<W> {
self.window.size()
}
/// Returns an animated size of the tile for rendering and input.
///
/// During the window opening animation, windows to the right should gradually slide further to
/// the right. This is what this visual size is used for. Other things like window resizes or
/// transactions or new view position calculation always use the real size, instead of this
/// visual size.
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
let size = self.tile_size();
let v = self
.open_animation
.as_ref()
.map(|anim| anim.value())
.unwrap_or(1.)
.max(0.);
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
}
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
loc += self.window_loc();
@@ -232,46 +295,92 @@ impl<W: LayoutElement> Tile<W> {
self.effective_border_width().is_some() || self.window.has_ssd()
}
pub fn render<R: Renderer + ImportAll>(
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<WorkspaceRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
let mut rv = Vec::new();
view_size: Size<i32, Logical>,
focus_ring: bool,
) -> impl Iterator<Item = TileRenderElement<R>> {
let rv = self
.window
.render(renderer, location + self.window_loc(), scale)
.into_iter()
.map(Into::into);
let window_pos = location + self.window_loc();
rv.extend(self.window.render(renderer, window_pos, scale));
let elem = self.effective_border_width().map(|width| {
self.border
.render(
renderer,
location + Point::from((width, width)),
scale,
view_size,
)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
if self.effective_border_width().is_some() {
rv.extend(
self.border
.render(scale)
.map(|elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
})
.map(Into::into),
);
}
let elem = focus_ring.then(|| {
self.focus_ring
.render(renderer, location, scale, view_size)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
if self.is_fullscreen {
let elem = SolidColorRenderElement::from_buffer(
let elem = self.is_fullscreen.then(|| {
SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
);
rv.push(elem.into());
}
)
.into()
});
rv.chain(elem)
}
rv
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
focus_ring: bool,
) -> impl Iterator<Item = TileRenderElement<R>> {
if let Some(anim) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
renderer,
scale.x as i32,
&elements,
anim.value().clamp(0., 1.) as f32,
);
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
let mut center = location;
center.x += self.tile_size().w / 2;
center.y += self.tile_size().h / 2;
Some(TileRenderElement::Offscreen(
RescaleRenderElement::from_element(
elem,
center.to_physical_precise_round(scale),
(anim.value() / 2. + 0.5).max(0.),
),
))
.into_iter()
.chain(None.into_iter().flatten())
} else {
self.window().set_offscreen_element_id(None);
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
}
+557 -192
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
#[macro_use]
extern crate tracing;
pub mod animation;
pub mod backend;
pub mod cli;
pub mod cursor;
#[cfg(feature = "dbus")]
pub mod dbus;
pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layout;
pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod rubber_band;
pub mod swipe_tracker;
pub mod ui;
pub mod utils;
pub mod window;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
+137 -108
View File
@@ -1,95 +1,33 @@
#[macro_use]
extern crate tracing;
mod animation;
mod backend;
mod config_error_notification;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod exit_confirm_dialog;
mod frame_clock;
mod handlers;
mod hotkey_overlay;
mod input;
mod ipc;
mod layout;
mod niri;
mod render_helpers;
mod screenshot_ui;
mod utils;
mod watcher;
#[cfg(not(feature = "xdp-gnome-screencast"))]
mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
mod pw_utils;
use std::ffi::OsString;
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::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
use clap::Parser;
use directories::ProjectDirs;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
use niri::{Niri, State};
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::{self, EventLoop};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
use utils::spawn;
use watcher::Watcher;
use crate::ipc::client::handle_msg;
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
/// Command to run upon compositor startup.
#[arg(last = true)]
command: Vec<OsString>,
#[command(subcommand)]
subcommand: Option<Sub>,
}
#[derive(Subcommand)]
enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
enum Msg {
/// List connected outputs.
Outputs,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
@@ -102,7 +40,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let is_systemd_service = env::var_os("NOTIFY_SOCKET").is_some();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
#[cfg(not(feature = "systemd"))]
warn!(
"running as a systemd service, but systemd support is compiled out. \
Are you sure you did not forget to set `--features systemd`?"
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
@@ -111,21 +57,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_env_filter(env_filter)
.init();
if is_systemd_service {
// If we're starting as a systemd service, 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.
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.
if env::var_os("DISPLAY").is_some() {
debug!("we're running as a systemd service but DISPLAY is set, removing it");
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
}
if env::var_os("WAYLAND_DISPLAY").is_some() {
debug!("we're running as a systemd service but WAYLAND_DISPLAY is set, removing it");
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
}
let cli = Cli::parse();
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
// Ensure the session type is set to Wayland for xdg-autostart and Qt apps.
env::set_var("XDG_SESSION_TYPE", "wayland");
}
let _client = tracy_client::Client::start();
@@ -154,7 +105,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("starting version {}", &version());
// Load the config.
let path = cli.config.or_else(default_config_path);
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
}
Some(default_path)
});
let mut config_errored = false;
let mut config = path
@@ -169,8 +157,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
})
.unwrap_or_default();
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
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);
// Create the compositor.
let mut event_loop = EventLoop::try_new().unwrap();
@@ -180,7 +175,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
);
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
@@ -196,9 +192,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
if is_systemd_service {
// We're starting as a systemd service. Export our variables.
import_env_to_systemd();
if cli.session {
// We're starting as a session. Import our variables.
import_environment();
// Inhibit power key handling so we can suspend on it.
#[cfg(feature = "dbus")]
@@ -210,15 +206,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
#[cfg(feature = "dbus")]
dbus::DBusServers::start(&mut state, is_systemd_service);
dbus::DBusServers::start(&mut state, cli.session);
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
// Send ready notification to the NOTIFY_FD file descriptor.
if let Err(err) = notify_fd() {
warn!("error notifying fd: {err:?}");
}
// Set up config file watcher.
let _watcher = if let Some(path) = path {
let _watcher = if let Some(path) = path.clone() {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
event_loop
@@ -243,6 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Show the config error notification right away if needed.
if config_errored {
state.niri.config_error_notification.show();
} else if config_created {
state.niri.config_error_notification.show_created(path);
}
// Run the compositor.
@@ -253,21 +256,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
fn import_environment() {
let variables = [
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
]
.join(" ");
let mut init_system_import = String::new();
if cfg!(feature = "systemd") {
write!(
init_system_import,
"systemctl --user import-environment {variables};"
)
.unwrap();
}
if cfg!(feature = "dinit") {
write!(init_system_import, "dinitctl setenv {variables};").unwrap();
}
fn import_env_to_systemd() {
let rv = Command::new("/bin/sh")
.args([
"-c",
"systemctl --user import-environment WAYLAND_DISPLAY && \
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment WAYLAND_DISPLAY",
&format!(
"{init_system_import}\
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment {variables}"
),
])
.spawn();
// Wait for the import process to complete, otherwise services will start too fast without
@@ -284,7 +301,7 @@ fn import_env_to_systemd() {
}
},
Err(err) => {
warn!("error spawning shell to import environment into systemd: {err:?}");
warn!("error spawning shell to import environment: {err:?}");
}
}
}
@@ -299,3 +316,15 @@ fn default_config_path() -> Option<PathBuf> {
path.push("config.kdl");
Some(path)
}
fn notify_fd() -> anyhow::Result<()> {
let fd = match env::var("NOTIFY_FD") {
Ok(notify_fd) => notify_fd.parse()?,
Err(env::VarError::NotPresent) => return Ok(()),
Err(err) => return Err(err.into()),
};
env::remove_var("NOTIFY_FD");
let mut notif = unsafe { File::from_raw_fd(fd) };
notif.write_all(b"READY=1\n")?;
Ok(())
}
+469 -501
View File
File diff suppressed because it is too large Load Diff
+466
View File
@@ -0,0 +1,466 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use arrayvec::ArrayVec;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
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 wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
const VERSION: u32 = 3;
pub struct ForeignToplevelManagerState {
display: DisplayHandle,
instances: Vec<ZwlrForeignToplevelManagerV1>,
toplevels: HashMap<WlSurface, ToplevelData>,
}
pub trait ForeignToplevelHandler {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
fn activate(&mut self, wl_surface: WlSurface);
fn close(&mut self, wl_surface: WlSurface);
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
}
struct ToplevelData {
title: Option<String>,
app_id: Option<String>,
states: ArrayVec<u32, 3>,
output: Option<Output>,
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
// FIXME: parent.
}
pub struct ForeignToplevelGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ForeignToplevelManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ForeignToplevelGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
instances: Vec::new(),
toplevels: HashMap::new(),
}
}
}
pub fn refresh(state: &mut State) {
let _span = tracy_client::span!("foreign_toplevel::refresh");
let protocol_state = &mut state.niri.foreign_toplevel_state;
// Handle closed windows.
protocol_state.toplevels.retain(|surface, data| {
if state.niri.layout.find_window_and_output(surface).is_some() {
return true;
}
for instance in data.instances.keys() {
instance.closed();
}
false
});
// Handle new and existing windows.
//
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|window, output| {
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();
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
} else {
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);
});
}
}
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
let Some(client) = wl_output.client() else {
return;
};
let protocol_state = &mut state.niri.foreign_toplevel_state;
for data in protocol_state.toplevels.values_mut() {
if data.output.as_ref() != Some(output) {
continue;
}
for (instance, outputs) in &mut data.instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
instance.output_enter(wl_output);
instance.done();
outputs.push(wl_output.clone());
}
}
}
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
role: &XdgToplevelSurfaceRoleAttributes,
output: Option<&Output>,
has_focus: bool,
) {
let states = to_state_vec(&role.current.states, has_focus);
match protocol_state.toplevels.entry(wl_surface.clone()) {
Entry::Occupied(entry) => {
// Existing window, check if anything changed.
let data = entry.into_mut();
let mut new_title = None;
if data.title != role.title {
data.title = role.title.clone();
new_title = role.title.as_deref();
if new_title.is_none() {
error!("toplevel title changed to None");
}
}
let mut new_app_id = None;
if data.app_id != role.app_id {
data.app_id = role.app_id.clone();
new_app_id = role.app_id.as_deref();
if new_app_id.is_none() {
error!("toplevel app_id changed to None");
}
}
let mut states_changed = false;
if data.states != states {
data.states = states;
states_changed = true;
}
let mut output_changed = false;
if data.output.as_ref() != output {
data.output = output.cloned();
output_changed = true;
}
let something_changed =
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
if something_changed {
for (instance, outputs) in &mut data.instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
if let Some(new_app_id) = new_app_id {
instance.app_id(new_app_id.to_owned());
}
if states_changed {
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
}
if output_changed {
for wl_output in outputs.drain(..) {
instance.output_leave(&wl_output);
}
if let Some(output) = &data.output {
if let Some(client) = instance.client() {
for wl_output in output.client_outputs(&client) {
instance.output_enter(&wl_output);
outputs.push(wl_output);
}
}
}
}
instance.done();
}
}
for outputs in data.instances.values_mut() {
// Clean up dead wl_outputs.
outputs.retain(|x| x.is_alive());
}
}
Entry::Vacant(entry) => {
// New window, start tracking it.
let mut data = ToplevelData {
title: role.title.clone(),
app_id: role.app_id.clone(),
states,
output: output.cloned(),
instances: HashMap::new(),
};
for manager in &protocol_state.instances {
if let Some(client) = manager.client() {
data.add_instance::<State>(&protocol_state.display, &client, manager);
}
}
entry.insert(data);
}
}
}
impl ToplevelData {
fn add_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
manager: &ZwlrForeignToplevelManagerV1,
) where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: 'static,
{
let toplevel = client
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
.unwrap();
manager.toplevel(&toplevel);
if let Some(title) = &self.title {
toplevel.title(title.clone());
}
if let Some(app_id) = &self.app_id {
toplevel.app_id(app_id.clone());
}
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
let mut outputs = Vec::new();
if let Some(output) = &self.output {
for wl_output in output.client_outputs(client) {
toplevel.output_enter(&wl_output);
outputs.push(wl_output);
}
}
toplevel.done();
self.instances.insert(toplevel, outputs);
}
}
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
for ForeignToplevelManagerState
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn bind(
state: &mut D,
handle: &DisplayHandle,
client: &Client,
resource: New<ZwlrForeignToplevelManagerV1>,
_global_data: &ForeignToplevelGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(resource, ());
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_instance::<D>(handle, client, &manager);
}
state.instances.push(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelManagerV1,
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
resource.finished();
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelManagerV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
}
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelHandleV1,
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let protocol_state = state.foreign_toplevel_manager_state();
let Some((surface, _)) = protocol_state
.toplevels
.iter()
.find(|(_, data)| data.instances.contains_key(resource))
else {
return;
};
let surface = surface.clone();
match request {
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
state.activate(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::Close => {
state.close(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
state.set_fullscreen(surface, output);
}
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
state.unset_fullscreen(surface);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelHandleV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.instances.retain(|instance, _| instance != resource);
}
}
}
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
let mut rv = ArrayVec::new();
if states.contains(xdg_toplevel::State::Maximized) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
}
if states.contains(xdg_toplevel::State::Fullscreen) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
}
// HACK: wlr-foreign-toplevel-management states:
//
// These have the same meaning as the states with the same names defined in xdg-toplevel
//
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
// handles multiple activated windows correctly uses it in its design in such a way that
// keyboard focus would make more sense. Let's do what the clients expect.
if has_focus {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
}
rv
}
#[macro_export]
macro_rules! delegate_foreign_toplevel {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
};
}
+2
View File
@@ -0,0 +1,2 @@
pub mod foreign_toplevel;
pub mod screencopy;
+386
View File
@@ -0,0 +1,386 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::wayland::shm;
// We do not support copy_with_damage() semantics yet.
const VERSION: u32 = 1;
pub struct ScreencopyManagerState;
pub struct ScreencopyManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ScreencopyManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ScreencopyManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
Self
}
}
impl<D> GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData, D>
for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_display: &DisplayHandle,
_client: &Client,
manager: New<ZwlrScreencopyManagerV1>,
_manager_state: &ScreencopyManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrScreencopyManagerV1, (), D> for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_manager: &ZwlrScreencopyManagerV1,
request: zwlr_screencopy_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let (frame, overlay_cursor, buffer_size, region_loc, output) = match request {
zwlr_screencopy_manager_v1::Request::CaptureOutput {
frame,
overlay_cursor,
output,
} => {
let output = Output::from_resource(&output).unwrap();
let buffer_size = output.current_mode().unwrap().size;
let region_loc = Point::from((0, 0));
(frame, overlay_cursor, buffer_size, region_loc, output)
}
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
frame,
overlay_cursor,
x,
y,
width,
height,
output,
} => {
if width <= 0 || height <= 0 {
trace!("screencopy client requested invalid sized region");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
}
let output = Output::from_resource(&output).unwrap();
let output_transform = output.current_transform();
let output_physical_size =
output_transform.transform_size(output.current_mode().unwrap().size);
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let output_scale = output.current_scale().integer_scale();
let physical_rect = rect.to_physical(output_scale);
// Clamp captured region to the output.
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
trace!("screencopy client requested region outside of output");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
};
let untransformed_rect = output_transform
.invert()
.transform_rect_in(clamped_rect, &output_physical_size);
(
frame,
overlay_cursor,
untransformed_rect.size,
clamped_rect.loc,
output,
)
}
zwlr_screencopy_manager_v1::Request::Destroy => return,
_ => unreachable!(),
};
// Create the frame.
let overlay_cursor = overlay_cursor != 0;
let info = ScreencopyFrameInfo {
output,
overlay_cursor,
buffer_size,
region_loc,
};
let frame = data_init.init(
frame,
ScreencopyFrameState::Pending {
info,
copied: Arc::new(AtomicBool::new(false)),
},
);
// Send desired SHM buffer parameters.
frame.buffer(
wl_shm::Format::Argb8888,
buffer_size.w as u32,
buffer_size.h as u32,
buffer_size.w as u32 * 4,
);
// if manager.version() >= 3 {
// // Send desired DMA buffer parameters.
// frame.linux_dmabuf(
// Fourcc::Argb8888 as u32,
// buffer_size.w as u32,
// buffer_size.h as u32,
// );
//
// // Notify client that all supported buffers were enumerated.
// frame.buffer_done();
// }
}
}
/// Handler trait for wlr-screencopy.
pub trait ScreencopyHandler {
/// Handle new screencopy request.
fn frame(&mut self, frame: Screencopy);
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! delegate_screencopy {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: $crate::protocols::screencopy::ScreencopyManagerGlobalData
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: ()
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::protocols::screencopy::ScreencopyFrameState
] => $crate::protocols::screencopy::ScreencopyManagerState);
};
}
#[derive(Clone)]
pub struct ScreencopyFrameInfo {
output: Output,
buffer_size: Size<i32, Physical>,
region_loc: Point<i32, Physical>,
overlay_cursor: bool,
}
pub enum ScreencopyFrameState {
Failed,
Pending {
info: ScreencopyFrameInfo,
copied: Arc<AtomicBool>,
},
}
impl<D> Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState, D> for ScreencopyManagerState
where
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
frame: &ZwlrScreencopyFrameV1,
request: zwlr_screencopy_frame_v1::Request,
data: &ScreencopyFrameState,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
if matches!(request, zwlr_screencopy_frame_v1::Request::Destroy) {
return;
}
let (info, copied) = match data {
ScreencopyFrameState::Failed => return,
ScreencopyFrameState::Pending { info, copied } => (info, copied),
};
if copied.load(Ordering::SeqCst) {
frame.post_error(
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
"copy was already requested",
);
return;
}
let (buffer, with_damage) = match request {
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
_ => unreachable!(),
};
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == info.buffer_size.w * 4
&& buffer_data.height == info.buffer_size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height
})
.unwrap_or(false)
{
frame.post_error(
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
"invalid buffer",
);
return;
}
copied.store(true, Ordering::SeqCst);
state.frame(Screencopy {
with_damage,
buffer,
frame: frame.clone(),
info: info.clone(),
submitted: false,
});
}
}
/// Screencopy frame.
pub struct Screencopy {
info: ScreencopyFrameInfo,
frame: ZwlrScreencopyFrameV1,
#[allow(unused)]
with_damage: bool,
buffer: WlBuffer,
submitted: bool,
}
impl Drop for Screencopy {
fn drop(&mut self) {
if !self.submitted {
self.frame.failed();
}
}
}
impl Screencopy {
/// Get the target buffer to copy to.
pub fn buffer(&self) -> &WlBuffer {
&self.buffer
}
pub fn region_loc(&self) -> Point<i32, Physical> {
self.info.region_loc
}
pub fn buffer_size(&self) -> Size<i32, Physical> {
self.info.buffer_size
}
pub fn output(&self) -> &Output {
&self.info.output
}
pub fn overlay_cursor(&self) -> bool {
self.info.overlay_cursor
}
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
// assert!(self.with_damage);
//
// for Rectangle { loc, size } in damage {
// self.frame
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
// }
// }
/// Submit the copied content.
pub fn submit(mut self, y_invert: bool) {
// Notify client that buffer is ordinary.
self.frame.flags(if y_invert {
Flags::YInvert
} else {
Flags::empty()
});
// Notify client about successful copy.
let time = UNIX_EPOCH.elapsed().unwrap();
let tv_sec_hi = (time.as_secs() >> 32) as u32;
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
let tv_nsec = time.subsec_nanos();
self.frame.ready(tv_sec_hi, tv_sec_lo, tv_nsec);
// Mark frame as submitted to ensure destructor isn't run.
self.submitted = true;
}
// pub fn submit_after_sync<T>(
// self,
// y_invert: bool,
// sync_point: Option<OwnedFd>,
// event_loop: &LoopHandle<'_, T>,
// ) {
// match sync_point {
// None => self.submit(y_invert),
// Some(sync_fd) => {
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
// let mut screencopy = Some(self);
// event_loop
// .insert_source(source, move |_, _, _| {
// screencopy.take().unwrap().submit(y_invert);
// Ok(PostAction::Remove)
// })
// .unwrap();
// }
// }
// }
}
+25 -19
View File
@@ -7,42 +7,48 @@ use std::rc::Rc;
use std::time::Duration;
use anyhow::Context as _;
use pipewire::spa::data::DataType;
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::context::Context;
use pipewire::core::Core;
use pipewire::main_loop::MainLoop;
use pipewire::properties::Properties;
use pipewire::spa::buffer::DataType;
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::spa::param::format_utils::parse_format;
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
use pipewire::spa::param::ParamType;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
use pipewire::spa::sys::*;
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
use pipewire::spa::Direction;
use pipewire::spa::utils::{
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
};
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
use pipewire::{Context, Core, MainLoop, Properties};
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::DrmDeviceFd;
use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
use crate::niri::State;
pub struct PipeWire {
_context: Context<MainLoop>,
_context: Context,
pub core: Core,
}
pub struct Cast {
pub session_id: usize,
pub stream: Rc<Stream>,
pub stream: Stream,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub size: Size<i32, Physical>,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
@@ -51,7 +57,7 @@ pub struct Cast {
impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new().context("error creating MainLoop")?;
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
@@ -66,14 +72,14 @@ impl PipeWire {
struct AsFdWrapper(MainLoop);
impl AsFd for AsFdWrapper {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.fd()
self.0.loop_().fd()
}
}
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
event_loop
.insert_source(generic, move |_, wrapper, _| {
let _span = tracy_client::span!("pipewire iteration");
wrapper.0.iterate(Duration::ZERO);
wrapper.0.loop_().iterate(Duration::ZERO);
Ok(PostAction::Continue)
})
.unwrap();
@@ -112,13 +118,14 @@ impl PipeWire {
let mode = output.current_mode().unwrap();
let size = mode.size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
.context("error creating Stream")?;
// Like in good old wayland-rs times...
let stream = Rc::new(stream);
let node_id = Rc::new(Cell::new(None));
let is_active = Rc::new(Cell::new(false));
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
@@ -127,10 +134,9 @@ impl PipeWire {
let listener = stream
.add_local_listener_with_user_data(())
.state_changed({
let stream = stream.clone();
let is_active = is_active.clone();
let stop_cast = stop_cast.clone();
move |old, new| {
move |stream, (), old, new| {
debug!("pw stream: state changed: {old:?} -> {new:?}");
match new {
@@ -174,7 +180,7 @@ impl PipeWire {
})
.param_changed({
let min_time_between_frames = min_time_between_frames.clone();
move |stream, id, _data, pod| {
move |stream, (), id, pod| {
let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed");
@@ -256,8 +262,7 @@ impl PipeWire {
let mut b1 = vec![];
// let mut b2 = vec![];
let mut params = [
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
];
stream.update_params(&mut params).unwrap();
}
@@ -265,7 +270,7 @@ impl PipeWire {
.add_buffer({
let dmabufs = dmabufs.clone();
let stop_cast = stop_cast.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: add_buffer");
unsafe {
@@ -309,7 +314,7 @@ impl PipeWire {
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: remove_buffer");
unsafe {
@@ -383,6 +388,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+135
View File
@@ -0,0 +1,135 @@
use glam::Vec2;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
use super::renderer::NiriRenderer;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
#[derive(Debug)]
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
impl GradientRenderElement {
pub fn new(
renderer: &mut impl NiriRenderer,
scale: Scale<f64>,
area: Rectangle<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
) -> Option<Self> {
let shader = Shaders::get(renderer).gradient_border.clone()?;
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
let grad_dir = Vec2::from_angle(angle);
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
let mut grad_area_diag = Vec2::new(w, h);
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
grad_area_diag.x = -w;
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y <= 0. {
grad_vec = -grad_vec;
}
let elem = PixelShaderElement::new(
shader,
area,
None,
1.,
vec![
Uniform::new("color_from", color_from),
Uniform::new("color_to", color_to),
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
],
Kind::Unspecified,
);
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
}
}
impl Element for GradientRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for GradientRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+169
View File
@@ -0,0 +1,169 @@
use std::ptr;
use anyhow::{ensure, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
use smithay::wayland::shm;
pub mod gradient;
pub mod offscreen;
pub mod primary_gpu_pixel_shader;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
pub mod shaders;
pub fn render_to_texture(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
let _span = tracy_client::span!();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
renderer
.bind(texture.clone())
.context("error binding texture")?;
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
Ok((texture, sync_point))
}
pub fn render_and_download(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!();
let (_, sync_point) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
sync_point.wait();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
pub fn render_to_vec(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<Vec<u8>> {
let _span = tracy_client::span!();
let mapping = render_and_download(renderer, size, scale, transform, fourcc, elements)
.context("error rendering")?;
let copy = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
Ok(copy.to_vec())
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let _span = tracy_client::span!();
renderer.bind(dmabuf).context("error binding texture")?;
render_elements(renderer, size, scale, transform, elements)
}
pub fn render_to_shm(
renderer: &mut GlesRenderer,
buffer: &WlBuffer,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
ensure!(
// The buffer prefers pixels in little endian ...
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == size.w * 4
&& buffer_data.height == size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
"invalid buffer format or size"
);
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
unsafe {
let _span = tracy_client::span!("copy_nonoverlapping");
ptr::copy_nonoverlapping(bytes.as_ptr(), shm_buffer.cast(), shm_len);
}
Ok(())
})
.context("expected shm buffer, but didn't get one")?
}
fn render_elements(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let transform = transform.invert();
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
.context("error starting frame")?;
frame
.clear([0., 0., 0., 0.], &[output_rect])
.context("error clearing")?;
for element in elements {
let src = element.src();
let dst = element.geometry(scale);
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
frame.finish().context("error finishing frame")
}
+216
View File
@@ -0,0 +1,216 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders elements into an off-screen buffer.
#[derive(Debug)]
pub struct OffscreenRenderElement {
// The texture, if rendering succeeded.
texture: Option<PrimaryGpuTextureRenderElement>,
// The fallback buffer in case the rendering fails.
fallback: SolidColorRenderElement,
}
impl OffscreenRenderElement {
pub fn new(
renderer: &mut GlesRenderer,
scale: i32,
elements: &[impl RenderElement<GlesRenderer>],
result_alpha: f32,
) -> Self {
let _span = tracy_client::span!("OffscreenRenderElement::new");
let geo = elements
.iter()
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let logical_size = geo.size.to_logical(scale);
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
let fallback = SolidColorRenderElement::from_buffer(
&fallback_buffer,
geo.loc,
Scale::from(scale as f64),
result_alpha,
Kind::Unspecified,
);
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
});
match render_to_texture(
renderer,
geo.size,
Scale::from(scale as f64),
Transform::Normal,
Fourcc::Abgr8888,
elements,
) {
Ok((texture, _sync_point)) => {
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let element = TextureRenderElement::from_texture_buffer(
geo.loc.to_f64(),
&buffer,
Some(result_alpha),
None,
None,
Kind::Unspecified,
);
Self {
texture: Some(PrimaryGpuTextureRenderElement(element)),
fallback,
}
}
Err(err) => {
warn!("error off-screening elements: {err:?}");
Self {
texture: None,
fallback,
}
}
}
}
}
impl Element for OffscreenRenderElement {
fn id(&self) -> &Id {
if let Some(texture) = &self.texture {
texture.id()
} else {
self.fallback.id()
}
}
fn current_commit(&self) -> CommitCounter {
if let Some(texture) = &self.texture {
texture.current_commit()
} else {
self.fallback.current_commit()
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
if let Some(texture) = &self.texture {
texture.geometry(scale)
} else {
self.fallback.geometry(scale)
}
}
fn transform(&self) -> Transform {
if let Some(texture) = &self.texture {
texture.transform()
} else {
self.fallback.transform()
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
if let Some(texture) = &self.texture {
texture.src()
} else {
self.fallback.src()
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
if let Some(texture) = &self.texture {
texture.damage_since(scale, commit)
} else {
self.fallback.damage_since(scale, commit)
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
if let Some(texture) = &self.texture {
texture.opaque_regions(scale)
} else {
self.fallback.opaque_regions(scale)
}
}
fn alpha(&self) -> f32 {
if let Some(texture) = &self.texture {
texture.alpha()
} else {
self.fallback.alpha()
}
}
fn kind(&self) -> Kind {
if let Some(texture) = &self.texture {
texture.kind()
} else {
self.fallback.kind()
}
}
}
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
@@ -0,0 +1,97 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a poxel shader from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
impl Element for PrimaryGpuPixelShaderRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
@@ -1,81 +1,12 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ 'static;
}
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
@@ -142,16 +73,14 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
}
}
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
for PrimaryGpuTextureRenderElement
{
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
@@ -159,7 +88,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render, 'alloc>,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
+148
View File
@@ -0,0 +1,148 @@
// We need to implement RenderElement manually due to AsGlesFrame requirement.
// This macro does it for us.
#[macro_export]
macro_rules! niri_render_elements {
// The two callable variants: with <R> and without <R>. They include From impls because nested
// repetitions ($type and $variant with + and $R with ?) don't work properly.
($name:ident<R> => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name () ($name<R>) => { $($variant = $type),+ });
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name ($name) () => { $($variant = $type),+ });
$(impl From<$type> for $name {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
// The internal variant that generates most of the code. $name_no_R and $name_R are necessary
// for the impl RenderElement<SomeRenderer> for $name<SomeRenderer>: since $R does not appear
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
#[derive(Debug)]
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
$($variant($type)),+
}
impl$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? smithay::backend::renderer::element::Element for $name$(<$R>)? {
fn id(&self) -> &smithay::backend::renderer::element::Id {
match self {
$($name::$variant(elem) => elem.id()),+
}
}
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
match self {
$($name::$variant(elem) => elem.current_commit()),+
}
}
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> smithay::utils::Rectangle<i32, smithay::utils::Physical> {
match self {
$($name::$variant(elem) => elem.geometry(scale)),+
}
}
fn transform(&self) -> smithay::utils::Transform {
match self {
$($name::$variant(elem) => elem.transform()),+
}
}
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
match self {
$($name::$variant(elem) => elem.src()),+
}
}
fn damage_since(
&self,
scale: smithay::utils::Scale<f64>,
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
match self {
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
}
}
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
match self {
$($name::$variant(elem) => elem.opaque_regions(scale)),+
}
}
fn alpha(&self) -> f32 {
match self {
$($name::$variant(elem) => elem.alpha()),+
}
}
fn kind(&self) -> smithay::backend::renderer::element::Kind {
match self {
$($name::$variant(elem) => elem.kind()),+
}
}
}
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(
&self,
renderer: &mut $crate::backend::tty::TtyRenderer<'render>,
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
};
}
+73
View File
@@ -0,0 +1,73 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use crate::backend::tty::{TtyFrame, TtyRenderer};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ 'static;
}
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
@@ -0,0 +1,35 @@
precision mediump float;
uniform float alpha;
#if defined(DEBUG_FLAGS)
uniform float tint;
#endif
uniform vec2 size;
varying vec2 v_coords;
uniform vec4 color_from;
uniform vec4 color_to;
uniform vec2 grad_offset;
uniform float grad_width;
uniform vec2 grad_vec;
void main() {
vec2 coords = v_coords * size + grad_offset;
if ((grad_vec.x < 0.0 && 0.0 <= grad_vec.y) || (0.0 <= grad_vec.x && grad_vec.y < 0.0))
coords.x -= grad_width;
float frac = dot(coords, grad_vec) / dot(grad_vec, grad_vec);
if (grad_vec.y < 0.0)
frac += 1.0;
frac = clamp(frac, 0.0, 1.0);
vec4 out_color = mix(color_from, color_to, frac);
#if defined(DEBUG_FLAGS)
if (tint == 1.0)
out_color = vec4(0.0, 0.3, 0.0, 0.2) + out_color * 0.8;
#endif
gl_FragColor = out_color;
}
+46
View File
@@ -0,0 +1,46 @@
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
use super::renderer::NiriRenderer;
pub struct Shaders {
pub gradient_border: Option<GlesPixelProgram>,
}
impl Shaders {
fn compile(renderer: &mut GlesRenderer) -> Self {
let _span = tracy_client::span!("Shaders::compile");
let gradient_border = renderer
.compile_custom_pixel_shader(
include_str!("gradient_border.frag"),
&[
UniformName::new("color_from", UniformType::_4f),
UniformName::new("color_to", UniformType::_4f),
UniformName::new("grad_offset", UniformType::_2f),
UniformName::new("grad_width", UniformType::_1f),
UniformName::new("grad_vec", UniformType::_2f),
],
)
.map_err(|err| {
warn!("error compiling gradient border shader: {err:?}");
})
.ok();
Self { gradient_border }
}
pub fn get(renderer: &mut impl NiriRenderer) -> &Self {
let renderer = renderer.as_gles_renderer();
let data = renderer.egl_context().user_data();
data.get()
.expect("shaders::init() must be called when creating the renderer")
}
}
pub fn init(renderer: &mut GlesRenderer) {
let shaders = Shaders::compile(renderer);
let data = renderer.egl_context().user_data();
if !data.insert_if_missing(|| shaders) {
error!("shaders were already compiled");
}
}
+39
View File
@@ -0,0 +1,39 @@
#[derive(Debug, Clone, Copy)]
pub struct RubberBand {
pub stiffness: f64,
pub limit: f64,
}
impl RubberBand {
pub fn band(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
(1. - (1. / (x * c / d + 1.))) * d
}
pub fn derivative(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
c * d * d / (c * x + d).powi(2)
}
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
let clamped = x.clamp(min, max);
let sign = if x < clamped { -1. } else { 1. };
let diff = (x - clamped).abs();
clamped + sign * self.band(diff)
}
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
if min <= x && x <= max {
return 1.;
}
let clamped = x.clamp(min, max);
let diff = (x - clamped).abs();
self.derivative(diff)
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::collections::VecDeque;
use std::time::Duration;
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
const DECELERATION_TOUCHPAD: f64 = 0.997;
#[derive(Debug)]
pub struct SwipeTracker {
history: VecDeque<Event>,
pos: f64,
}
#[derive(Debug, Clone, Copy)]
struct Event {
delta: f64,
timestamp: Duration,
}
impl SwipeTracker {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
history: VecDeque::new(),
pos: 0.,
}
}
/// Pushes a new reading into the tracker.
pub fn push(&mut self, delta: f64, timestamp: Duration) {
// For the events that we care about, timestamps should always increase
// monotonically.
if let Some(last) = self.history.back() {
if timestamp < last.timestamp {
trace!(
"ignoring event with timestamp {timestamp:?} earlier than last {:?}",
last.timestamp
);
return;
}
}
self.history.push_back(Event { delta, timestamp });
self.pos += delta;
self.trim_history();
}
/// Returns the current gesture position.
pub fn pos(&self) -> f64 {
self.pos
}
/// Computes the current gesture velocity.
pub fn velocity(&self) -> f64 {
let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else {
return 0.;
};
let total_time = (last.timestamp - first.timestamp).as_secs_f64();
if total_time == 0. {
return 0.;
}
let total_delta = self.history.iter().map(|event| event.delta).sum::<f64>();
total_delta / total_time
}
/// Computes the gesture end position after decelerating to a halt.
pub fn projected_end_pos(&self) -> f64 {
let vel = self.velocity();
self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln())
}
fn trim_history(&mut self) {
let Some(&Event { timestamp, .. }) = self.history.back() else {
return;
};
while let Some(first) = self.history.front() {
if timestamp <= first.timestamp + HISTORY_LIMIT {
break;
}
let _ = self.history.pop_front();
}
}
}
@@ -1,7 +1,10 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Duration;
use niri_config::Config;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::FontDescription;
use smithay::backend::renderer::element::memory::{
@@ -14,7 +17,7 @@ use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::animation::Animation;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Failed to parse the config file. \
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
@@ -26,6 +29,12 @@ const BORDER: i32 = 4;
pub struct ConfigErrorNotification {
state: State,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
// notification.
created_path: Option<PathBuf>,
config: Rc<RefCell<Config>>,
}
enum State {
@@ -39,16 +48,43 @@ pub type ConfigErrorNotificationRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ConfigErrorNotification {
pub fn new() -> Self {
pub fn new(config: Rc<RefCell<Config>>) -> Self {
Self {
state: State::Hidden,
buffers: RefCell::new(HashMap::new()),
created_path: None,
config,
}
}
fn animation(&self, from: f64, to: f64) -> Animation {
let c = self.config.borrow();
Animation::new(
from,
to,
0.,
c.animations.config_notification_open_close,
niri_config::Animation::default_config_notification_open_close(),
)
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
}
self.state = State::Showing(self.animation(0., 1.));
}
pub fn show(&mut self) {
if self.created_path.is_some() {
self.created_path = None;
self.buffers.borrow_mut().clear();
}
// Show from scratch even if already showing to bring attention.
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
self.state = State::Showing(self.animation(0., 1.));
}
pub fn hide(&mut self) {
@@ -56,7 +92,7 @@ impl ConfigErrorNotification {
return;
}
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
self.state = State::Hiding(self.animation(1., 0.));
}
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
@@ -65,7 +101,15 @@ impl ConfigErrorNotification {
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
let duration = if self.created_path.is_some() {
// Make this quite a bit longer because it comes with a monitor modeset
// (can take a while) and an important hotkeys popup diverting the
// attention.
Duration::from_secs(8)
} else {
Duration::from_secs(4)
};
self.state = State::Shown(target_presentation_time + duration);
}
}
State::Shown(deadline) => {
@@ -75,7 +119,9 @@ impl ConfigErrorNotification {
}
State::Hiding(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
// HACK: prevent bounciness on hiding. This is better done with a clamp property on
// the spring animation.
if anim.is_done() || anim.value() <= 0. {
self.state = State::Hidden;
}
}
@@ -96,11 +142,12 @@ impl ConfigErrorNotification {
}
let scale = output.current_scale().integer_scale();
let path = self.created_path.as_deref();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale).ok());
.or_insert_with_key(move |&scale| render(scale, path).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
@@ -138,19 +185,30 @@ impl ConfigErrorNotification {
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut text = String::from(TEXT);
let mut border_color = (1., 0.3, 0.3);
if let Some(path) = created_path {
text = format!(
"Created a default config file at \
<span face='monospace' bgcolor='#000000'>{:?}</span>",
path
);
border_color = (0.5, 1., 0.5);
};
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
@@ -166,25 +224,25 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(1., 0.3, 0.3);
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -12,7 +12,7 @@ use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
@@ -111,7 +111,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
@@ -130,13 +130,13 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -18,7 +18,7 @@ use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::{Physical, Size, Transform};
use crate::input::CompositorMod;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const PADDING: i32 = 8;
const MARGIN: i32 = PADDING * 2;
@@ -155,13 +155,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
let binds = &config.binds.0;
// Collect actions that we want to show.
let mut actions = vec![
&Action::ShowHotkeyOverlay,
&Action::Quit,
&Action::CloseWindow,
];
let mut actions = vec![&Action::ShowHotkeyOverlay];
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
// Quit(false).
if binds.iter().any(|bind| bind.action == Action::Quit(false)) {
actions.push(&Action::Quit(false));
} else if binds.iter().any(|bind| bind.action == Action::Quit(true)) {
actions.push(&Action::Quit(true));
} else {
actions.push(&Action::Quit(false));
}
actions.extend(&[
&Action::CloseWindow,
&Action::FocusColumnLeft,
&Action::FocusColumnRight,
&Action::MoveColumnLeft,
@@ -173,12 +180,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown))
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
{
actions.push(&Action::MoveColumnToWorkspaceDown);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
{
actions.push(&Action::MoveWindowToWorkspaceDown);
} else {
@@ -188,12 +195,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
// Same for -up.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
{
actions.push(&Action::MoveColumnToWorkspaceUp);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
{
actions.push(&Action::MoveWindowToWorkspaceUp);
} else {
@@ -208,20 +215,26 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
]);
// Screenshot is not as important, can omit if not bound.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
{
if binds.iter().any(|bind| bind.action == Action::Screenshot) {
actions.push(&Action::Screenshot);
}
// Add the spawn actions.
for bind in binds
.iter()
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
{
actions.push(bind.actions.first().unwrap());
let mut spawn_actions = Vec::new();
for bind in binds.iter().filter(|bind| {
matches!(bind.action, Action::Spawn(_))
// Only show binds with Mod or Super to filter out stuff like volume up/down.
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|| bind.key.modifiers.contains(Modifiers::SUPER))
}) {
let action = &bind.action;
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
if !spawn_actions.contains(&action) {
spawn_actions.push(action);
}
}
actions.extend(spawn_actions);
let strings = actions
.into_iter()
@@ -230,7 +243,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
.binds
.0
.iter()
.find(|bind| bind.actions.first() == Some(action))
.find(|bind| bind.action == *action)
.map(|bind| key_name(comp_mod, &bind.key))
.unwrap_or_else(|| String::from("(not bound)"));
@@ -243,7 +256,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
let bold = AttrList::new();
@@ -298,7 +311,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
cr.set_source_rgb(1., 1., 1.);
@@ -306,20 +319,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
layout.set_attributes(Some(&attrs));
layout.set_text(key);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to((key_width + padding).into(), 0.);
layout.set_attributes(None);
layout.set_markup(action);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to(
(-(key_width + padding)).into(),
@@ -338,7 +351,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -356,7 +369,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
fn action_name(action: &Action) -> String {
match action {
Action::Quit => String::from("Exit niri"),
Action::Quit(_) => String::from("Exit niri"),
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
Action::CloseWindow => String::from("Close Focused Window"),
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
+4
View File
@@ -0,0 +1,4 @@
pub mod config_error_notification;
pub mod exit_confirm_dialog;
pub mod hotkey_overlay;
pub mod screenshot_ui;
+17 -149
View File
@@ -10,16 +10,15 @@ use smithay::backend::allocator::Fourcc;
use smithay::backend::input::{ButtonState, MouseButton};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::ExportMem;
use smithay::input::keyboard::{Keysym, ModifiersState};
use smithay::output::{Output, WeakOutput};
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::PrimaryGpuTextureRenderElement;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
const BORDER: i32 = 2;
@@ -42,16 +41,18 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
transform: Transform,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
buffers: [SolidColorBuffer; 8],
locations: [Point<i32, Physical>; 8],
}
#[derive(Debug)]
pub enum ScreenshotUiRenderElement {
Screenshot(PrimaryGpuTextureRenderElement),
SolidColor(SolidColorRenderElement),
niri_render_elements! {
ScreenshotUiRenderElement => {
Screenshot = PrimaryGpuTextureRenderElement,
SolidColor = SolidColorRenderElement,
}
}
impl ScreenshotUi {
@@ -94,6 +95,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -104,9 +106,9 @@ impl ScreenshotUi {
let output_data = screenshots
.into_iter()
.map(|(output, texture)| {
let output_transform = output.current_transform();
let transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let size = transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
let texture_buffer = TextureBuffer::from_texture(
renderer,
@@ -129,6 +131,7 @@ impl ScreenshotUi {
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -333,10 +336,10 @@ impl ScreenshotUi {
}
}
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
if let Self::Open { output_data, .. } = self {
let data = output_data.get(output)?;
Some((data.size, data.scale))
Some((data.size, data.scale, data.transform))
} else {
None
}
@@ -448,138 +451,3 @@ pub fn rect_from_corner_points(
let y2 = max(a.y, b.y);
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
}
// Manual RenderElement implementation due to AsGlesFrame requirement.
impl Element for ScreenshotUiRenderElement {
fn id(&self) -> &Id {
match self {
Self::Screenshot(elem) => elem.id(),
Self::SolidColor(elem) => elem.id(),
}
}
fn current_commit(&self) -> CommitCounter {
match self {
Self::Screenshot(elem) => elem.current_commit(),
Self::SolidColor(elem) => elem.current_commit(),
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
match self {
Self::Screenshot(elem) => elem.geometry(scale),
Self::SolidColor(elem) => elem.geometry(scale),
}
}
fn transform(&self) -> Transform {
match self {
Self::Screenshot(elem) => elem.transform(),
Self::SolidColor(elem) => elem.transform(),
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
match self {
Self::Screenshot(elem) => elem.src(),
Self::SolidColor(elem) => elem.src(),
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
match self {
Self::Screenshot(elem) => elem.damage_since(scale, commit),
Self::SolidColor(elem) => elem.damage_since(scale, commit),
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
match self {
Self::Screenshot(elem) => elem.opaque_regions(scale),
Self::SolidColor(elem) => elem.opaque_regions(scale),
}
}
fn alpha(&self) -> f32 {
match self {
Self::Screenshot(elem) => elem.alpha(),
Self::SolidColor(elem) => elem.alpha(),
}
}
fn kind(&self) -> Kind {
match self {
Self::Screenshot(elem) => elem.kind(),
Self::SolidColor(elem) => elem.kind(),
}
}
}
impl RenderElement<GlesRenderer> for ScreenshotUiRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
match self {
Self::Screenshot(elem) => {
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
}
Self::SolidColor(elem) => {
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
}
}
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for ScreenshotUiRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'render, 'alloc, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
match self {
Self::Screenshot(elem) => {
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
}
Self::SolidColor(elem) => {
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
}
}
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render, 'alloc>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl From<SolidColorRenderElement> for ScreenshotUiRenderElement {
fn from(x: SolidColorRenderElement) -> Self {
Self::SolidColor(x)
}
}
impl From<PrimaryGpuTextureRenderElement> for ScreenshotUiRenderElement {
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
Self::Screenshot(x)
}
}
+28 -86
View File
@@ -1,25 +1,36 @@
use std::ffi::{CString, OsStr};
use std::io::{self, Write};
use std::io::Write;
use std::os::unix::prelude::OsStrExt;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::path::{Path, PathBuf};
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{ensure, Context};
use directories::UserDirs;
use git_version::git_version;
use niri_config::Config;
use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size};
pub mod spawning;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
}
pub fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
pub fn get_monotonic_time() -> Duration {
let ts = clock_gettime(ClockId::Monotonic);
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
@@ -39,6 +50,15 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
.to_logical(output_scale)
}
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
Ok(Some([dirs.home_dir(), rest].iter().collect()))
} else {
Ok(None)
}
}
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
let Some(path) = &config.screenshot_path else {
return Ok(None);
@@ -61,91 +81,13 @@ pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>>
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
}
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
path = [dirs.home_dir(), rest].iter().collect();
if let Some(expanded) = expand_home(&path).context("error expanding ~")? {
path = expanded;
}
Ok(Some(path))
}
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let command = command.as_ref();
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Double-fork to avoid having to waitpid the child.
unsafe {
process.pre_exec(|| {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
Ok(())
});
}
let mut child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return;
}
};
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
pub fn write_png_rgba8(
w: impl Write,
width: u32,
+324
View File
@@ -0,0 +1,324 @@
use std::ffi::OsStr;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::{io, thread};
use libc::close_range;
use niri_config::Environment;
use smithay::reexports::rustix;
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
use crate::utils::expand_home;
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let mut command = command.as_ref();
// Expand `~` at the start.
let expanded = expand_home(Path::new(command));
match &expanded {
Ok(Some(expanded)) => command = expanded.as_ref(),
Ok(None) => (),
Err(err) => {
warn!("error expanding ~: {err:?}");
}
}
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Set configured environment.
let env = CHILD_ENV.read().unwrap();
for var in &env.0 {
if let Some(value) = &var.value {
process.env(&var.name, value);
} else {
process.env_remove(&var.name);
}
}
drop(env);
// When running as a systemd session, we want to put children into their own transient scopes
// in order to separate them from the niri process. This is helpful for example to prevent the
// OOM killer from taking down niri together with a misbehaving client.
//
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits at
// just the right time, the transient unit will be created but empty, so it will linger around
// forever.
//
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
// intermediate child we will send back the grandchild PID, and in niri we will create a
// transient scope with both our intermediate child and the grandchild PIDs set. Only then we
// will signal our intermediate child to exit. This way, even if the grandchild exits quickly,
// a non-empty scope will be created (with just our intermediate child), then cleaned up when
// our intermediate child exits.
// Make a pipe to receive the grandchild PID.
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe to transfer child PID: {err:?}");
})
.ok()
.unzip();
// Make a pipe to wait in the intermediate child.
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe for child to wait on: {err:?}");
})
.ok()
.unzip();
unsafe {
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
// the raw fd inside so that it's not closed any extra times.
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
// Close FDs that we don't need. Especially important for the write ones to unblock the
// readers.
if let Some(fd) = pipe_pid_read_fd.take() {
close(fd);
}
if let Some(fd) = pipe_wait_write_fd.take() {
close(fd);
}
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
grandchild_pid => {
// Send back the PID.
if let Some(pipe) = pipe_pid_write {
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
}
// Wait until the parent signals us to exit.
if let Some(pipe) = pipe_wait_read {
// We're going to exit afterwards. Close all other FDs to allow
// Command::spawn() to return in the parent process.
let raw = pipe.as_raw_fd() as u32;
let _ = close_range(0, raw - 1, 0);
let _ = close_range(raw + 1, !0, 0);
let _ = read_all(pipe, &mut [0]);
}
libc::_exit(0)
}
}
Ok(())
});
}
let mut child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return;
}
};
drop(pipe_pid_write);
drop(pipe_wait_read);
// Wait for the grandchild PID.
if let Some(pipe) = pipe_pid_read {
let mut buf = [0; 4];
match read_all(pipe, &mut buf) {
Ok(()) => {
let pid = i32::from_ne_bytes(buf);
trace!("spawned PID: {pid}");
// Start a systemd scope for the grandchild.
#[cfg(feature = "systemd")]
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
trace!("error starting systemd scope for spawned command: {err:?}");
}
}
Err(err) => {
warn!("error reading child PID: {err:?}");
}
}
}
// Signal the intermediate child to exit now that we're done trying to creating a systemd scope.
trace!("signaling child to exit");
drop(pipe_wait_write);
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
let mut written = 0;
loop {
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
written += n;
if written == buf.len() {
return Ok(());
}
}
}
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
let mut start = 0;
loop {
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
start += n;
if start == buf.len() {
return Ok(());
}
}
}
/// Puts a (newly spawned) pid into a transient systemd scope.
///
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
/// from bringing down the compositor together with a misbehaving client.
#[cfg(feature = "systemd")]
fn start_systemd_scope(name: &OsStr, intermediate_pid: u32, child_pid: u32) -> anyhow::Result<()> {
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
use anyhow::Context;
use zbus::zvariant::{OwnedObjectPath, Value};
use crate::utils::IS_SYSTEMD_SERVICE;
// We only start transient scopes if we're a systemd service ourselves.
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
return Ok(());
}
let _span = tracy_client::span!();
// Extract the basename.
let name = Path::new(name).file_name().unwrap_or(name);
let mut scope_name = String::from("app-niri-");
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
// systemd source.
for &c in name.as_bytes() {
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
scope_name.push(char::from(c));
} else {
let _ = write!(scope_name, "\\x{c:02x}");
}
}
let _ = write!(scope_name, "-{child_pid}.scope");
// Ask systemd to start a transient scope.
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
let conn = CONNECTION
.get_or_init(zbus::blocking::Connection::session)
.clone()
.context("error connecting to session bus")?;
let proxy = zbus::blocking::Proxy::new(
&conn,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
)
.context("error creating a Proxy")?;
let signals = proxy
.receive_signal("JobRemoved")
.context("error creating a signal iterator")?;
let pids: &[_] = &[intermediate_pid, child_pid];
let properties: &[_] = &[
("PIDs", Value::new(pids)),
("CollectMode", Value::new("inactive-or-failed")),
];
let aux: &[(&str, &[(&str, Value)])] = &[];
let job: OwnedObjectPath = proxy
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
.context("error calling StartTransientUnit")?;
trace!("waiting for JobRemoved");
for message in signals {
let body: (u32, OwnedObjectPath, &str, &str) =
message.body().context("error parsing signal")?;
if body.1 == job {
// Our transient unit had started, we're good to exit the intermediate child.
break;
}
}
Ok(())
}
+348
View File
@@ -0,0 +1,348 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
Self::with_start_notification(path, changed, None)
}
pub fn with_start_notification(
path: PathBuf,
changed: SyncSender<()>,
started: Option<mpsc::SyncSender<()>>,
) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
// this "should" be as simple as mtime, but it does not quite work in practice;
// it doesn't work if the config is a symlink, and its target changes but the
// new target and old target have identical mtimes.
//
// in practice, this does not occur on any systems other than nix.
// because, on nix practically everything is a symlink to /nix/store
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
let mut last_props = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
.ok();
if let Some(started) = started {
let _ = started.send(());
}
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(new_props) = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
{
if last_props.as_ref() != Some(&new_props) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_props = Some(new_props);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::AtomicU8;
use calloop::channel::sync_channel;
use calloop::EventLoop;
use smithay::reexports::rustix::fs::{futimens, Timestamps};
use smithay::reexports::rustix::time::Timespec;
use xshell::{cmd, Shell};
use super::*;
fn check(
setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
) {
let sh = Shell::new().unwrap();
let temp_dir = sh.create_temp_dir().unwrap();
sh.change_dir(temp_dir.path());
// let dir = sh.create_dir("xshell").unwrap();
// sh.change_dir(dir);
let mut config_path = sh.current_dir();
config_path.push("niri");
config_path.push("config.kdl");
setup(&sh).unwrap();
let changed = AtomicU8::new(0);
let mut event_loop = EventLoop::try_new().unwrap();
let loop_handle = event_loop.handle();
let (tx, rx) = sync_channel(1);
let (started_tx, started_rx) = mpsc::sync_channel(1);
let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
loop_handle
.insert_source(rx, |_, _, _| {
changed.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
started_rx.recv().unwrap();
// HACK: if we don't sleep, files might have the same mtime.
thread::sleep(Duration::from_millis(100));
change(&sh).unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 1);
// Verify that the watcher didn't break.
sh.write_file(&config_path, "c").unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 2);
}
#[test]
fn change_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn create_file() {
check(
|sh| {
sh.create_dir("niri")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn create_dir_and_file() {
check(
|_sh| Ok(()),
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn change_linked_file() {
check(
|sh| {
sh.write_file("niri/config2.kdl", "a")?;
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
sh.write_file("niri/config2.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn change_file_in_linked_dir() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri/config.kdl")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn swap_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
sh.remove_path("niri")?;
cmd!(sh, "mv niri2 niri").run()?;
Ok(())
},
);
}
#[test]
fn swap_just_link() {
// NixOS setup: link path changes, mtime stays constant.
check(
|sh| {
let mut dir = sh.current_dir();
dir.push("niri");
sh.create_dir(&dir)?;
let mut d2 = dir.clone();
d2.push("config2.kdl");
let mut c2 = File::create(d2).unwrap();
write!(c2, "a")?;
c2.flush()?;
futimens(
&c2,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c2.sync_all()?;
drop(c2);
let mut d3 = dir.clone();
d3.push("config3.kdl");
let mut c3 = File::create(d3).unwrap();
write!(c3, "b")?;
c3.flush()?;
futimens(
&c3,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c3.sync_all()?;
drop(c3);
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
cmd!(sh, "unlink niri/config.kdl").run()?;
cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
Ok(())
},
);
}
#[test]
fn swap_dir_link() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri3/config.kdl", "b")?;
cmd!(sh, "unlink niri").run()?;
cmd!(sh, "ln -s niri3 niri").run()?;
Ok(())
},
);
}
}
-60
View File
@@ -1,60 +0,0 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_mtime = Some(mtime);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
+80
View File
@@ -0,0 +1,80 @@
use smithay::desktop::Window;
use smithay::output::Output;
use crate::layout::workspace::ColumnWidth;
#[derive(Debug)]
pub struct Unmapped {
pub window: Window,
pub state: InitialConfigureState,
}
#[derive(Debug)]
pub enum InitialConfigureState {
/// The window has not been initially configured yet.
NotConfigured {
/// Whether the window requested to be fullscreened, and the requested output, if any.
wants_fullscreen: Option<Option<Output>>,
},
/// The window has been configured.
Configured {
/// Up-to-date rules.
///
/// We start tracking window rules when sending the initial configure, since they don't
/// affect anything before that.
rules: ResolvedWindowRules,
/// Resolved default width for this window.
///
/// `None` means that the window will pick its own width.
width: Option<ColumnWidth>,
/// Whether the window should open full-width.
is_full_width: bool,
/// Output to open this window on.
///
/// This can be `None` in cases like:
///
/// - There are no outputs connected.
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
/// should fetch the parent's current output again upon mapping.
output: Option<Output>,
},
}
/// Rules fully resolved for a window.
#[derive(Debug, Default)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
}
impl Unmapped {
/// Wraps a newly created window that hasn't been initially configured yet.
pub fn new(window: Window) -> Self {
Self {
window,
state: InitialConfigureState::NotConfigured {
wants_fullscreen: None,
},
}
}
pub fn needs_initial_configure(&self) -> bool {
matches!(self.state, InitialConfigureState::NotConfigured { .. })
}
}