Compare commits

..

248 Commits

Author SHA1 Message Date
Ivan Molodetskikh 88b74f4a3a Remove unnecessary crop bounds during workspace switch 2024-02-13 09:32:49 +04:00
Ivan Molodetskikh b94b0c7fa4 Use nearest scaling for integer-upscaled surfaces 2024-02-13 09:32:49 +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
Ivan Molodetskikh 6945ccde18 Bump version to 0.1.0-beta.1 2024-01-20 09:38:42 +04:00
Ivan Molodetskikh e86e9c6c9a CI: Add a Fedora build 2024-01-20 09:25:50 +04:00
Ivan Molodetskikh dc47de178f Add an option to skip the hotkey overlay at startup 2024-01-20 08:31:05 +04:00
Ivan Molodetskikh 65e864965e Print git version in clap too 2024-01-19 20:46:10 +04:00
Ivan Molodetskikh 55ad36addc layout: Fix crash due to workspace transfer during switch 2024-01-19 20:24:59 +04:00
Ivan Molodetskikh 26c8cbb961 layout: Fix crash due to workspace cleanup during switch 2024-01-19 20:24:18 +04:00
Ivan Molodetskikh 031133c052 README: Add link to important software wiki page 2024-01-19 07:01:56 -08:00
Ivan Molodetskikh a6f821d3fa Update dependencies 2024-01-19 09:41:16 +04:00
Ivan Molodetskikh 475b3df2b5 Don't crash when failing to render a cursor
I only hit this when the renderer was completely busted, but
nevertheless.
2024-01-19 09:13:32 +04:00
Ivan Molodetskikh 1541835f00 Prettify Return => Enter key 2024-01-19 08:35:36 +04:00
Ivan Molodetskikh 4b9cb2f0d3 Add exit confirmation dialog 2024-01-19 08:33:54 +04:00
Ivan Molodetskikh 3461c66d2c Redraw upon starting PW stream
Otherwise it may take a while for the first frame to arrive.
2024-01-18 21:16:36 +04:00
Ivan Molodetskikh 011c91c98a Add an important hotkeys overlay 2024-01-18 20:32:44 +04:00
Ivan Molodetskikh edafa139f6 portal: Name and sort monitors, fix session restore
xdp-gnome restores by a combination of model + make + serial. We
currently can't set those reliably (until libdisplay-info most monitors
will have them unknown) so pass the connector name instead. This will
work as expected in most cases.
2024-01-18 16:31:04 +04:00
Ivan Molodetskikh fa9b3ed106 Add a config parse error notification
We can't rely on a notification daemon being available, especially
during initial niri setup. So, render our own.
2024-01-18 12:44:05 +04:00
Ivan Molodetskikh cc62a403c0 Update Smithay (deadlock fix) 2024-01-18 11:14:39 +04:00
Ivan Molodetskikh 0f85c79548 Watch config path even if it didn't exist at startup 2024-01-18 11:13:36 +04:00
Ivan Molodetskikh 6beef26662 Fix dependency sorting 2024-01-18 11:00:49 +04:00
Ivan Molodetskikh 616055e205 Update README.md 2024-01-17 03:15:05 -08:00
Ivan Molodetskikh 40c85da102 Add an IPC socket and a niri msg outputs subcommand 2024-01-17 10:45:18 +04:00
Ivan Molodetskikh 768b326028 Rename connectors to enabled_outputs 2024-01-17 10:25:23 +04:00
Ivan Molodetskikh f068157f55 Add a calloop futures executor 2024-01-17 10:24:01 +04:00
Ivan Molodetskikh 6703d5ce72 tty: Add Tracy span to on_output_config_changed() 2024-01-17 10:21:40 +04:00
Ivan Molodetskikh 12590f689a Write a comment on xdg-decoration lack of live-reload 2024-01-16 20:43:28 +04:00
Ivan Molodetskikh 4656332d07 Add live-reload to libinput settings 2024-01-16 20:29:37 +04:00
Ivan Molodetskikh 954f711bf3 Extract apply_libinput_settings() 2024-01-16 20:28:37 +04:00
Ivan Molodetskikh c09c964420 default-config: Add example for spawn with bash 2024-01-16 20:08:31 +04:00
Ivan Molodetskikh 1f9abaaa58 Add live-reload for output mode 2024-01-16 18:02:30 +04:00
Ivan Molodetskikh eb4946c3d8 tty: Extract pick_mode() 2024-01-16 18:01:25 +04:00
Ivan Molodetskikh 5f440f7be3 Add live-reload for output on/off 2024-01-16 15:34:00 +04:00
Ivan Molodetskikh 6644cc16ff tty: Remove connector arg from connector_disconnected() 2024-01-16 15:33:37 +04:00
Ivan Molodetskikh 9e667efc4c Close layer surfaces upon output removal
Fixes https://github.com/YaLTeR/niri/issues/23
2024-01-16 13:28:29 +04:00
Ivan Molodetskikh 8a7e4bc3cd Add Tracy span to Config::load and parse 2024-01-16 12:53:40 +04:00
Ivan Molodetskikh 69907f123d Add live-reload of output scales 2024-01-16 11:34:34 +04:00
Ivan Molodetskikh 6ca3b6ddb5 Move output scale setting into niri 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh fc5a080ca5 layout: Fix surface leaving output when consuming into column 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh 83719a49b7 Add live-reload of output positions 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh da4967d43c Reposition all outputs on any change
This way the positioning is independent of the order of plugging in.
2024-01-16 08:43:28 +04:00
Ivan Molodetskikh d958a9679c Change message from debug to trace 2024-01-16 07:38:52 +04:00
Ivan Molodetskikh e4643c6dbe Implement security-context, hide some protocols from it 2024-01-15 16:02:07 +04:00
Ivan Molodetskikh 59763fd0da Hide decoration globals when we need CSD
This gets the current SDL2 with libdecor working.
2024-01-15 16:01:01 +04:00
Ivan Molodetskikh 533659eef8 Update Smithay 2024-01-15 15:59:36 +04:00
Ivan Molodetskikh 81443d8e16 Change default binds to move columns instead of windows 2024-01-15 11:51:04 +04:00
Ivan Molodetskikh fb38ae26c9 Add move-column-to-monitor* binds
As opposed to move-window-to-monitor*
2024-01-15 10:36:59 +04:00
Ivan Molodetskikh cc4acdf24a Add move-column-to-workspace* binds
As opposed to move-window-to-workspace*
2024-01-15 10:31:44 +04:00
Ivan Molodetskikh 2506d43bb9 xdg-decoration: Document SDL2 bug 2024-01-14 09:28:03 +04:00
Ivan Molodetskikh d899bc4712 Revert "Be more insistent on CSD by default"
This reverts commit 43e2cf14d2.

SDL2 until very recently (unreleased version) has had a bug where
changing the decoration mode to client-side during its initial window
creation would keep the window permanently hidden. Breaking all SDL2
apps for years to come is unfortunately not a good solution.
2024-01-14 09:23:15 +04:00
Ivan Molodetskikh 14552d856c xdg-decoration: Always send configure
The protocol wording seems to require it.
2024-01-14 08:57:46 +04:00
Ivan Molodetskikh 632a00fcca Implement popup grabs 2024-01-13 09:00:57 +04:00
Ivan Molodetskikh 80652a0765 Remove is_grabbed check for changing active window
When clicking outside of the popup grab, the click does go through if
the popup is dismissed. This makes the active window change go through
too.
2024-01-13 08:17:53 +04:00
Ivan Molodetskikh a52bf92ae1 Add missing screen redraws on focus changes
The window isn't guaranteed to commit a buffer.
2024-01-13 08:17:53 +04:00
Ivan Molodetskikh 952ff02982 Keep track of keyboard focus manually 2024-01-12 17:14:18 +04:00
Ivan Molodetskikh e1adabed2d Rename update_focus -> update_keyboard_focus 2024-01-12 16:53:00 +04:00
Ivan Molodetskikh b5c4f9ed2a Remove obsolete FIXME comment
It's implemented now.
2024-01-12 14:54:59 +04:00
Ivan Molodetskikh d39f1897c7 Force redraws on window activation
Activating a window does not necessarily make it commit a buffer and
update the screen for us.
2024-01-12 08:48:22 +04:00
Ivan Molodetskikh e46b614c2b Fix clicks activating windows through layer-shell surfaces 2024-01-12 08:45:39 +04:00
Ivan Molodetskikh 78aa08b100 Silence the two type complexity lints
meh
2024-01-11 22:10:12 +04:00
Ivan Molodetskikh d8626fcab0 Fix clippy suggestion 2024-01-11 21:42:00 +04:00
Ivan Molodetskikh f4e04ac910 Mark cause_panic() as #[inline(never)]
Despite compiling with frame pointers, inlining cause_panic() makes the
backtrace omit its frame and even the source location in main...
2024-01-11 18:30:54 +04:00
Bill Sun 236abd9d9d Add Nix Flake (#77)
* Add Nix Flake

Co-authored-by: Bryce Berger <bryce.z.berger@gmail.com>

* Describe nix flake in readme

* Add `niri-config` to build source list

* Add maintainer info

Add comment at top to indicate the Nix Flake file
is community maintained.

* Clarify Nix/NixOS README instructions

* Shorten Nix/NixOS build instructions

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

* Move NixOS installation instruction to "Tip" section

---------

Co-authored-by: Bryce Berger <bryce.z.berger@gmail.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-01-10 22:43:46 -08:00
Ivan Molodetskikh b2df3e104f Document debug settings in the default config 2024-01-09 08:18:34 +04:00
Ivan Molodetskikh ec2d339a86 Add panic subcommand to check backtraces 2024-01-09 08:08:38 +04:00
Ivan Molodetskikh 629a2ccb47 layout: Improve Options randomization in tests 2024-01-08 20:57:53 +04:00
Thomas Versteeg fb93038bd8 Add center-focused-column setting 2024-01-08 17:37:18 +04:00
Ivan Molodetskikh 71fef2ad2e Add a few mouse libinput settings 2024-01-08 11:53:34 +04:00
Ivan Molodetskikh c6841f19e9 Add touchpad tap-button-map setting 2024-01-08 10:32:04 +04:00
Ivan Molodetskikh e1971c4af5 Add touchpad dwt setting 2024-01-08 10:24:00 +04:00
Ivan Molodetskikh 07b1d0e98d Add touchpad accel-profile setting 2024-01-08 10:23:53 +04:00
Ivan Molodetskikh ffe25f5cc4 Update Smithay and dependencies 2024-01-07 17:44:08 +04:00
Ivan Molodetskikh 43e2cf14d2 Be more insistent on CSD by default
By default I want CSD so that people don't necessarily have to learn all
of the shortcuts right away, and can at least use the X to close
windows.
2024-01-07 09:41:56 +04:00
Ivan Molodetskikh 2c59131f7f Remove debuginfo from niri-config
Finally, the file sizes are back to sane.
2024-01-07 09:28:50 +04:00
Ivan Molodetskikh 64c41fa2c8 Move config into a separate crate
Get miette and knuffel deps contained within.
2024-01-07 09:28:14 +04:00
Ivan Molodetskikh 4e0aa39113 [cfg-breaking] Move layout settings into their own scope 2024-01-06 13:04:21 +04:00
Ivan Molodetskikh dcb80efc88 Bump version to 0.1.0-alpha.3 2024-01-06 09:30:46 +04:00
Ivan Molodetskikh 3d5de921cd Add render-drm-device debug config option 2024-01-06 09:14:48 +04:00
Ivan Molodetskikh 8703feedee tty: Print full path to render node 2024-01-06 08:46:30 +04:00
Ivan Molodetskikh a27d22571d README: Link NixOS flake 2024-01-06 08:33:12 +04:00
Ivan Molodetskikh d10af92aea Redraw on toggling debug tint 2024-01-06 08:28:12 +04:00
Ivan Molodetskikh 0bc83eda71 Update Smithay and dependencies 2024-01-06 08:22:53 +04:00
Ivan Molodetskikh 6fce5662e7 Update Smithay 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh 1c7c5b3f28 tty: Avoid non-linear cross-device formats 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh b9d7812f1f Update dependencies 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh 655b9808b9 Update Smithay 2024-01-05 23:23:30 +04:00
Ivan Molodetskikh 5cd31e5730 Implement multi-GPU support
Rendering always happens on the primary GPU.
2024-01-03 18:16:20 +04:00
Ivan Molodetskikh de3fc2def0 Make rendering generic over renderer 2024-01-03 18:05:15 +04:00
Ivan Molodetskikh fd1d4b07fd Replace renderer() with with_primary_renderer() 2024-01-03 11:38:09 +04:00
Ivan Molodetskikh 8b5acd5e6e Rename RenderResult::Error to Skipped 2024-01-03 11:38:07 +04:00
Ivan Molodetskikh 31bb9096e2 Move Niri::render() invocation to backends 2024-01-03 11:38:02 +04:00
Ivan Molodetskikh dae93ee159 Render again for screencast
Will be needed when multi-gpu support is added.
2024-01-03 11:37:04 +04:00
Ivan Molodetskikh 57a7347620 Move import_dmabuf to backends 2024-01-03 11:37:04 +04:00
Ivan Molodetskikh 628891db2c Add disable-cursor-plane debug flag 2024-01-03 08:42:49 +04:00
Ivan Molodetskikh be6e25f5fb tty: Extract config variable 2024-01-03 08:42:49 +04:00
Ivan Molodetskikh e005a795e7 layout: Make generic over Renderer 2024-01-01 17:31:58 +04:00
Ivan Molodetskikh 655fe413fb tty: Don't store render formats
Actually, how did it even fork before? Pretty sure it was storing render
formats, not texture formats, but with render formats
weston-simple-dmabuf-feedback doesn't work?
2024-01-01 17:31:58 +04:00
Ivan Molodetskikh ac6ff7ff41 tty: Disable dmabuf global before destroying 2024-01-01 17:31:58 +04:00
Ivan Molodetskikh 84befb4e91 tty: Move dmabuf_global up into Tty 2024-01-01 09:54:45 +04:00
Ivan Molodetskikh d39f7bebf3 tty: Fix import sorting
One of the imports was too long so rustfmt bailed.
2024-01-01 09:40:41 +04:00
Ivan Molodetskikh 0dd9a42087 Move DmabufState to Niri 2023-12-31 12:02:39 +04:00
Ivan Molodetskikh 658941f2c3 Enable EGL for the winit backend
More relevant now that mesa on Fedora is trying to force non-working
Zink for some reason.
2023-12-31 09:46:22 +04:00
Ivan Molodetskikh 6ccc4147ae Use remove/insert for columns instead of swapping
Now with move-to-first/last this is relevant.
2023-12-29 16:58:18 +04:00
Ivan Molodetskikh 46d5f5ec4d Add randomized border to proptest 2023-12-29 11:43:16 +04:00
Ivan Molodetskikh c64e96d0d8 Add test with i32::MAX max_size 2023-12-29 11:42:54 +04:00
Ivan Molodetskikh 44d3a5b9a2 Use saturating_sub in window_height_for_tile_height() 2023-12-29 11:38:01 +04:00
Ivan Molodetskikh 5d95de97a0 Add new binds to the README 2023-12-29 08:09:17 +04:00
Ivan Molodetskikh 56174b2c34 Add move-column-to-{first,last} 2023-12-29 08:01:02 +04:00
Ivan Molodetskikh 310aa2b464 layout: Extract move_column_to() 2023-12-29 08:00:40 +04:00
Ivan Molodetskikh d6c553091f Add focus-column-{first,last} 2023-12-29 07:51:14 +04:00
Ivan Molodetskikh 097c415036 layout: Use saturating_add() in several places
Apparently VSCode sends i32::MAX worth of max_size?
2023-12-28 18:53:08 +04:00
Ivan Molodetskikh 2d16c04869 Fix unfullscreening not always updating view offset 2023-12-28 10:49:31 +04:00
Ivan Molodetskikh 249f2b7a21 Add disable-power-key-handling option 2023-12-28 09:36:10 +04:00
Ivan Molodetskikh f3e5e13c45 Activate windows only on active monitors
This matches what we do in advance_animations().
2023-12-28 09:09:28 +04:00
Ivan Molodetskikh b13892ca63 Activate windows when clicking on the border 2023-12-28 08:45:12 +04:00
Ivan Molodetskikh 777ad4ee5c Rename window -> tile, fix popup unconstraining window_y
I missed the window_y thing and found it thanks to the rename. Too lazy
to split it into a commit.
2023-12-28 08:25:42 +04:00
Ivan Molodetskikh c21805bf70 layout: Refactor to support window decorations, add border and fullscreen backdrop
Windows are now wrapped in Tiles, which keep track of window-specific
decorations. Particularly, I implemented a black fullscreen backdrop,
which finally brings fullscreened windows smaller than the screen in
line with how the Wayland protocol says they should look—centered in a
black rectangle. I also implemented window borders, which are similar to
the focus ring, but always visible (and hence affect the layout and
sizing).
2023-12-27 21:51:42 +04:00
Ivan Molodetskikh bfc2418267 Make LayoutElement more visual-geometry-based 2023-12-26 17:40:25 +04:00
Ivan Molodetskikh 77b4715e0b Use correct type alias in FocusRing::render 2023-12-26 17:28:52 +04:00
Ivan Molodetskikh c048abc8b5 layout: Add Column::position 2023-12-26 10:29:00 +04:00
Ivan Molodetskikh 4dd7578fe7 layout: Use column.contains more 2023-12-25 11:06:03 +04:00
Ivan Molodetskikh ea72e4dae8 README: Add Xwayland wiki link 2023-12-24 19:41:23 +04:00
Ivan Molodetskikh 0c671ee493 Remove dependency of LayoutElement on SpaceElement 2023-12-24 18:18:44 +04:00
Ivan Molodetskikh 324c1efd04 Remove Clone bound from LayoutElement
It is no longer necessary.
2023-12-24 18:02:13 +04:00
Ivan Molodetskikh 99e75b95b7 Remove unnecessary window clone 2023-12-24 18:02:03 +04:00
Ivan Molodetskikh 489a225fae Add and use remove_window_by_idx 2023-12-24 18:00:46 +04:00
Ivan Molodetskikh 85cb4b42f6 Remove clones from active_window 2023-12-24 17:40:30 +04:00
Ivan Molodetskikh be2e551a89 Move clones up from find_window_and_output 2023-12-24 17:38:13 +04:00
Ivan Molodetskikh ed3080d908 Split layout mod into files
No functional change intended.
2023-12-24 15:10:09 +04:00
Ivan Molodetskikh 461ce5f363 Move output_size to utils 2023-12-24 14:30:19 +04:00
Ivan Molodetskikh 624c799ebf Move layout.rs into its own module 2023-12-24 14:20:50 +04:00
68 changed files with 16048 additions and 6460 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)
+70 -19
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,15 +33,10 @@ 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
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:
@@ -56,17 +52,18 @@ jobs:
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
clippy:
visual-tests:
strategy:
fail-fast: false
name: clippy
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -75,15 +72,37 @@ 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
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
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --package niri-visual-tests
clippy:
strategy:
fail-fast: false
name: clippy
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal --component clippy
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
with:
components: clippy
- uses: Swatinem/rust-cache@v2
@@ -107,3 +126,35 @@ jobs:
- name: Run rustfmt
run: cargo fmt --all -- --check
fedora:
runs-on: ubuntu-22.04
container: fedora:39
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
- uses: Swatinem/rust-cache@v2
- 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
+1
View File
@@ -1 +1,2 @@
/target
/result
Generated
+927 -388
View File
File diff suppressed because it is too large Load Diff
+64 -29
View File
@@ -1,48 +1,82 @@
[package]
name = "niri"
version = "0.1.0-alpha.2"
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
anyhow = "1.0.79"
bitflags = "2.4.2"
clap = { version = "4.4.18", features = ["derive"] }
serde = { version = "1.0.196", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.16.5", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/YaLTeR/smithay.git"
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
git = "https://github.com/YaLTeR/smithay.git"
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
# path = "../smithay/smithay-drm-extras"
[package]
name = "niri"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
readme = "README.md"
repository = "https://github.com/YaLTeR/niri"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.75" }
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.1"
clap = { version = "4.4.11", features = ["derive"] }
bitflags = "2.4.2"
calloop = { version = "0.12.4", 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"
input = { version = "0.9.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
knuffel = "3.2.0"
libc = "0.2.151"
logind-zbus = { version = "3.1.2", optional = true }
libc = "0.2.153"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
miette = "5.10.0"
niri-config = { version = "0.1.1", path = "niri-config" }
niri-ipc = { version = "0.1.1", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pipewire = { version = "0.7.2", optional = true }
png = "0.17.10"
pangocairo = "0.19.1"
pipewire = { version = "0.8.0", optional = true }
png = "0.17.11"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.12"
profiling = "1.0.14"
sd-notify = "0.4.1"
serde = { version = "1.0.193", features = ["derive"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.4", default-features = false }
serde.workspace = true
serde_json = "1.0.113"
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
zbus = { version = "3.15.0", optional = true }
[dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
workspace = true
features = [
"backend_drm",
"backend_egl",
@@ -53,15 +87,12 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
]
[dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
@@ -69,7 +100,7 @@ proptest-derive = "0.4.0"
[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"]
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
@@ -80,8 +111,12 @@ debug = "line-tables-only"
overflow-checks = true
lto = "thin"
[profile.release.package.niri-config]
# knuffel with chomsky generates a metric ton of debuginfo.
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~alpha.2"
version = "0.1.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+94 -36
View File
@@ -1,58 +1,92 @@
# niri
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
A scrollable-tiling Wayland compositor.
![](https://github.com/YaLTeR/niri/assets/1794388/16f87a4a-afac-49aa-b3e6-5e6f16c943a9)
![](https://github.com/YaLTeR/niri/assets/1794388/e35fd9e1-105b-4bd5-94c9-207fd6fb3c18)
## About
Windows are arranged in columns on an infinite strip going to the right.
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.
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
- 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.
## Idea
## Inspiration
Niri implements scrollable tiling, heavily inspired by [PaperWM].
Windows are arranged in columns on an infinite strip going to the right.
Every column takes up a full monitor worth of height, divided among its windows.
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
With multiple monitors, every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
This is one of the reasons that prompted me to try writing my own compositor.
PaperWM is a solid implementation, but, being a GNOME Shell extension, it has to work around Shell's global window coordinate space to prevent windows from overflowing.
Niri also has dynamic workspaces which work similar to GNOME Shell.
Since windows go left-to-right horizontally, workspaces are arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
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.
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.
## Building
For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
> [!TIP]
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> NixOS users, check out https://github.com/sodiboo/niri-flake
>
> For Arch users, there's an AUR package: https://aur.archlinux.org/packages/niri
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
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
```
- Fedora:
```sh
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel clang
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
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
```
nix run --impure github:guibou/nixGL -- ./results/bin/niri
```
## Installation
@@ -91,6 +125,23 @@ A step-by-step process for this is explained [on the wiki](https://github.com/Ya
Niri also works with some parts of xdg-desktop-portal-gnome.
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
### Xwayland
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
### IPC
You can communicate with the running niri instance over an IPC socket.
Check `niri msg --help` for available commands.
The `--json` flag prints the response in JSON, rather than formatted.
For example, `niri msg --json outputs`.
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
The communication over the IPC socket happens in JSON.
## Default Hotkeys
When running on a TTY, the Mod key is <kbd>Super</kbd>.
@@ -100,6 +151,7 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| Hotkey | Description |
| ------ | ----------- |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
@@ -112,14 +164,16 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused window to the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused window to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused window to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>1</kbd><kbd>9</kbd> | Switch to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused window to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused column to a workspace by index |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
@@ -144,11 +198,15 @@ Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or
If this fails, it will load [the default configuration file](resources/default-config.kdl).
Please use the default configuration file as the starting point for your custom configuration.
Niri will live-reload many of the configuration settings, like key binds or gaps, as you change the config file.
Though, some settings are still missing live-reload support.
Notably, output modes and positions will only apply when the output is reconnected.
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
[PaperWM]: https://github.com/paperwm/PaperWM
[mako]: https://github.com/emersion/mako
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
Generated
+138
View File
@@ -0,0 +1,138 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1707685877,
"narHash": "sha256-XoXRS+5whotelr1rHiZle5t5hDg9kpguS5yk8c8qzOc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "2c653e4478476a52c6aa3ac0495e4dea7449ea0e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1706768574,
"narHash": "sha256-4o6TMpzBHO659EiJTzd/EGQGUDdbgwKwhqf3u6b23U8=",
"owner": "nix-community",
"repo": "fenix",
"rev": "668102037129923cd0fc239d864fce71eabdc6a3",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1707619277,
"narHash": "sha256-vKnYD5GMQbNQyyQm4wRlqi+5n0/F1hnvqSQgaBy4BqY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f3a93440fbfff8a74350f4791332a19282cc6dc8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1706735270,
"narHash": "sha256-IJk+UitcJsxzMQWm9pa1ZbJBriQ4ginXOlPyVq+Cu40=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "42cb1a2bd79af321b0cc503d2960b73f34e2f92b",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+102
View File
@@ -0,0 +1,102 @@
# This flake file is community maintained
# Maintainers:
# Bill Sun (github/billksun)
{
description = "Niri: A scrollable-tiling Wayland compositor.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nix-filter.url = "github:numtide/nix-filter";
fenix = {
url = "github:nix-community/fenix/monthly";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
crane,
nix-filter,
flake-utils,
fenix,
...
}: let
systems = ["aarch64-linux" "x86_64-linux"];
in
flake-utils.lib.eachSystem systems (
system: let
pkgs = nixpkgs.legacyPackages.${system};
toolchain = fenix.packages.${system}.complete.toolchain;
craneLib = crane.lib.${system}.overrideToolchain toolchain;
craneArgs = {
pname = "niri";
version = self.rev or "dirty";
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
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
];
runtimeDependencies = with pkgs; [
wayland
mesa
libglvnd # For libEGL
];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
in {
formatter = pkgs.alejandra;
checks.niri = niri;
packages.default = niri;
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
packages = niri.runtimeDependencies;
# Force linking to libEGL, which is always dlopen()ed, and to
# libwayland-client, which is always dlopen()ed except by the
# obscure winit backend.
RUSTFLAGS = map (a: "-C link-arg=${a}") [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
];
};
}
);
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "niri-config"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
bitflags.workspace = true
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.1", path = "../niri-ipc" }
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "niri-ipc"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
serde.workspace = true
[features]
clap = ["dep:clap"]
+312
View File
@@ -0,0 +1,312 @@
//! Types for communicating with niri via IPC.
#![warn(missing_docs)]
use std::collections::HashMap;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Request {
/// Request information about connected outputs.
Outputs,
/// Perform an action.
Action(Action),
}
/// 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 {
/// Name of the output.
pub name: String,
/// Textual description of the manufacturer.
pub make: String,
/// Textual description of the model.
pub model: String,
/// Physical width and height of the output in millimeters, if known.
pub physical_size: Option<(u32, u32)>,
/// Available modes for the output.
pub modes: Vec<Mode>,
/// Index of the current mode in [`Self::modes`].
///
/// `None` if the output is disabled.
pub current_mode: Option<usize>,
}
/// Output mode.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct Mode {
/// Width in physical pixels.
pub width: u16,
/// Height in physical pixels.
pub height: u16,
/// 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.0", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.1", path = ".." }
niri-config = { version = "0.1.1", 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;
}
+228
View File
@@ -0,0 +1,228 @@
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),
},
..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()
}
}
+22
View File
@@ -0,0 +1,22 @@
use std::time::Duration;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
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>>>;
}
+111
View File
@@ -0,0 +1,111 @@
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.), 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()
}
}
+165
View File
@@ -0,0 +1,165 @@
#[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::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",
);
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();
}
+245
View File
@@ -0,0 +1,245 @@
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::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);
GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")
}
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
}
}
+256 -88
View File
@@ -27,8 +27,18 @@ input {
// Omitting settings disables them, or leaves them at their default values.
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
}
mouse {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
tablet {
@@ -37,9 +47,16 @@ input {
// 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
// (i.e. logind.conf).
// disable-power-key-handling
}
// You can configure outputs by their name, which you can find with wayland-info(1).
// 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 "/-"!
/-output "eDP-1" {
@@ -49,12 +66,16 @@ 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.
// All valid modes are listed in niri's debug output when an output is connected.
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@144"
// Position of the output in the global coordinate space.
@@ -69,26 +90,88 @@ input {
position x=1280 y=0
}
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 decoratins 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.
// off
// 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
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
}
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// 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.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
// - "always", the focused column will always be centered.
center-focused-column "never"
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// spawn-at-startup "alacritty" "-e" "fish"
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// 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
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
@@ -101,39 +184,6 @@ cursor {
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
// prefer-no-csd
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// 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.5
proportion 0.667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
// You can change the path where screenshots are saved.
// A ~ at the front will be expanded to the home directory.
// The path is formatted with strftime(3) to give you the screenshot date and time.
@@ -142,6 +192,62 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// You can also set this to null to disable saving screenshots to disk.
// screenshot-path null
// Settings for the "Important Hotkeys" overlay.
hotkey-overlay {
// Uncomment this line if you don't want to see the hotkey help at niri startup.
// 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.
// - 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".
// Animation when switching workspaces up and down,
// including after the touchpad gesture.
workspace-switch {
// off
// duration-ms 250
// curve "ease-out-cubic"
}
// 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
// duration-ms 250
// curve "ease-out-cubic"
}
// Window opening animation. Note that this one has different defaults.
window-open {
// off
// duration-ms 150
// curve "ease-out-expo"
}
// Config parse error and new default config creation notification
// open/close animation.
config-notification-open-close {
// off
// duration-ms 250
// curve "ease-out-cubic"
}
}
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
@@ -149,11 +255,21 @@ 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.
Mod+Shift+Slash { show-hotkey-overlay; }
// 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"; }
// Example volume keys mappings for PipeWire & WirePlumber.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
@@ -161,23 +277,23 @@ binds {
Mod+Q { close-window; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
Mod+Ctrl+Left { move-column-left; }
Mod+Ctrl+Down { move-window-down; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
// Alternative commands that move across workspaces when reaching
// the first or last window in a column.
@@ -186,37 +302,54 @@ binds {
// Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
// Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Down { focus-monitor-down; }
Mod+Shift+Up { focus-monitor-up; }
Mod+Shift+Right { focus-monitor-right; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Shift+Ctrl+H { move-window-to-monitor-left; }
Mod+Shift+Ctrl+J { move-window-to-monitor-down; }
Mod+Shift+Ctrl+K { move-window-to-monitor-up; }
Mod+Shift+Ctrl+L { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-window-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-window-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// Alternatively, there are commands to move just a single window:
// 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+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+Ctrl+U { move-window-to-workspace-down; }
Mod+Ctrl+I { move-window-to-workspace-up; }
Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
Mod+Ctrl+Page_Up { move-window-to-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
// ...
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
@@ -227,19 +360,26 @@ binds {
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Ctrl+1 { move-window-to-workspace 1; }
Mod+Ctrl+2 { move-window-to-workspace 2; }
Mod+Ctrl+3 { move-window-to-workspace 3; }
Mod+Ctrl+4 { move-window-to-workspace 4; }
Mod+Ctrl+5 { move-window-to-workspace 5; }
Mod+Ctrl+6 { move-window-to-workspace 6; }
Mod+Ctrl+7 { move-window-to-workspace 7; }
Mod+Ctrl+8 { move-window-to-workspace 8; }
Mod+Ctrl+9 { move-window-to-workspace 9; }
Mod+Ctrl+1 { move-column-to-workspace 1; }
Mod+Ctrl+2 { move-column-to-workspace 2; }
Mod+Ctrl+3 { move-column-to-workspace 3; }
Mod+Ctrl+4 { move-column-to-workspace 4; }
Mod+Ctrl+5 { move-column-to-workspace 5; }
Mod+Ctrl+6 { move-column-to-workspace 6; }
Mod+Ctrl+7 { move-column-to-workspace 7; }
Mod+Ctrl+8 { move-column-to-workspace 8; }
Mod+Ctrl+9 { move-column-to-workspace 9; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
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; }
@@ -272,8 +412,36 @@ 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; }
}
// Settings for debugging. Not meant for normal use.
// These can change or stop working at any point with little notice.
debug {
// Make niri take over its DBus services even if it's not running as a session.
// Useful for testing screen recording changes without having to relogin.
// The main niri instance will *not* currently take back the services; so you will
// need to relogin in the end.
// dbus-interfaces-in-non-session-instances
// Wait until every frame is done rendering before handing it over to DRM.
// wait-for-frame-completion-before-queueing
// Enable direct scanout into overlay planes.
// May cause frame drops during some animations on some hardware.
// enable-overlay-planes
// Disable the use of the cursor plane.
// The cursor will be rendered together with the rest of the frame.
// disable-cursor-plane
// Override the DRM device that niri will use for all rendering.
// render-drm-device "/dev/dri/renderD129"
}
+1 -1
View File
@@ -44,4 +44,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
+49 -3
View File
@@ -15,20 +15,43 @@ pub struct Animation {
duration: Duration,
start_time: Duration,
current_time: Duration,
curve: Curve,
}
#[derive(Debug, Clone, Copy)]
pub enum Curve {
EaseOutCubic,
EaseOutExpo,
}
impl Animation {
pub fn new(from: f64, to: f64, over: Duration) -> Self {
pub fn new(
from: f64,
to: f64,
config: niri_config::Animation,
default: niri_config::Animation,
) -> 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_ms = if config.off {
0
} else {
config.duration_ms.unwrap_or(default.duration_ms.unwrap())
};
let duration = Duration::from_millis(u64::from(duration_ms))
.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed));
let curve = Curve::from(config.curve.unwrap_or(default.curve.unwrap()));
Self {
from,
to,
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
duration,
start_time: now,
current_time: now,
curve,
}
}
@@ -44,10 +67,33 @@ impl Animation {
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
self.curve.y(x) * (self.to - self.from) + self.from
}
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,
}
}
}
+46 -20
View File
@@ -1,13 +1,16 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::OutputRenderElements;
use crate::Niri;
use crate::niri::Niri;
pub mod tty;
pub use tty::Tty;
@@ -26,8 +29,8 @@ pub enum RenderResult {
Submitted,
/// Rendering succeeded, but there was no damage.
NoDamage,
/// An error has occurred, the frame was not submitted.
Error,
/// The frame was not rendered and submitted, due to an error or otherwise.
Skipped,
}
impl Backend {
@@ -45,10 +48,13 @@ impl Backend {
}
}
pub fn renderer(&mut self) -> Option<&mut GlesRenderer> {
pub fn with_primary_renderer<T>(
&mut self,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
match self {
Backend::Tty(tty) => tty.renderer(),
Backend::Winit(winit) => Some(winit.renderer()),
Backend::Tty(tty) => tty.with_primary_renderer(f),
Backend::Winit(winit) => winit.with_primary_renderer(f),
}
}
@@ -56,12 +62,11 @@ impl Backend {
&mut self,
niri: &mut Niri,
output: &Output,
elements: &[OutputRenderElements<GlesRenderer>],
target_presentation_time: Duration,
) -> RenderResult {
match self {
Backend::Tty(tty) => tty.render(niri, output, elements, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output, elements),
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output),
}
}
@@ -93,11 +98,32 @@ impl Backend {
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self {
Backend::Tty(tty) => tty.connectors(),
Backend::Winit(winit) => winit.connectors(),
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
}
}
pub fn early_import(&mut self, surface: &WlSurface) {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
}
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
match self {
Backend::Tty(tty) => tty.enabled_outputs(),
Backend::Winit(winit) => winit.enabled_outputs(),
}
}
@@ -107,21 +133,21 @@ impl Backend {
) -> Option<smithay::backend::allocator::gbm::GbmDevice<smithay::backend::drm::DrmDeviceFd>>
{
match self {
Backend::Tty(tty) => tty.gbm_device(),
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
}
}
pub fn is_active(&self) -> bool {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.is_active(),
Backend::Winit(_) => true,
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
}
}
+934 -315
View File
File diff suppressed because it is too large Load Diff
+77 -54
View File
@@ -5,46 +5,41 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, Renderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
use smithay::backend::winit::{self, WinitEvent, WinitGraphicsBackend};
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::config::Config;
use crate::niri::{OutputRenderElements, RedrawState, State};
use crate::niri::{Niri, RedrawState, State};
use crate::utils::get_monotonic_time;
use crate::Niri;
pub struct Winit {
config: Rc<RefCell<Config>>,
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
connectors: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
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 output_config = config
.borrow()
.outputs
.iter()
.find(|o| o.name == "winit")
.cloned()
.unwrap_or_default();
let (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -60,16 +55,27 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
let scale = output_config.scale.clamp(1., 10.).ceil() as i32;
output.change_current_state(
Some(mode),
Some(Transform::Flipped180),
Some(Scale::Integer(scale)),
None,
);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let connectors = Arc::new(Mutex::new(HashMap::from([(
let physical_properties = output.physical_properties();
let ipc_outputs = Rc::new(RefCell::new(HashMap::from([(
"winit".to_owned(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
refresh_rate: 60_000,
}],
current_mode: Some(0),
},
)])));
let enabled_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
output.clone(),
)])));
@@ -89,6 +95,12 @@ impl Winit {
None,
None,
);
let mut ipc_outputs = winit.ipc_outputs.borrow_mut();
let mode = &mut ipc_outputs.get_mut("winit").unwrap().modes[0];
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
state.niri.output_resized(winit.output.clone());
}
WinitEvent::Input(event) => state.process_input_event(event),
@@ -96,34 +108,29 @@ 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,
connectors,
}
ipc_outputs,
enabled_outputs,
})
}
pub fn init(&mut self, niri: &mut Niri) {
// For some reason, binding the display here causes damage tracker artifacts.
//
// use smithay::backend::renderer::ImportEgl;
//
// if let Err(err) = self
// .backend
// .renderer()
// .bind_wl_display(&niri.display_handle)
// {
// warn!("error binding renderer wl_display: {err}");
// }
if let Err(err) = self
.backend
.renderer()
.bind_wl_display(&niri.display_handle)
{
warn!("error binding renderer wl_display: {err}");
}
niri.add_output(self.output.clone(), None);
}
@@ -131,23 +138,25 @@ impl Winit {
"winit".to_owned()
}
pub fn renderer(&mut self) -> &mut GlesRenderer {
self.backend.renderer()
pub fn with_primary_renderer<T>(
&mut self,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
Some(f(self.backend.renderer()))
}
pub fn render(
&mut self,
niri: &mut Niri,
output: &Output,
elements: &[OutputRenderElements<GlesRenderer>],
) -> RenderResult {
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
let res = self
.damage_tracker
.render_output(self.backend.renderer(), age, elements, [0.; 4])
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
.unwrap();
niri.update_primary_scanout_output(output, &res.states);
@@ -202,7 +211,21 @@ impl Winit {
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self.backend.renderer().import_dmabuf(dmabuf, None) {
Ok(_texture) => true,
Err(err) => {
debug!("error importing dmabuf: {err:?}");
false
}
}
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
self.ipc_outputs.clone()
}
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.enabled_outputs.clone()
}
}
+55
View File
@@ -0,0 +1,55 @@
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>,
/// 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,
},
}
-816
View File
@@ -1,816 +0,0 @@
use std::path::PathBuf;
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig};
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
#[knuffel(child, default)]
pub input: Input,
#[knuffel(children(name = "output"))]
pub outputs: Vec<Output>,
#[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
#[knuffel(child, default)]
pub focus_ring: FocusRing,
#[knuffel(child, default)]
pub prefer_no_csd: bool,
#[knuffel(child, default)]
pub cursor: Cursor,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetWidth>,
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument), default = 16)]
pub gaps: u16,
#[knuffel(child, default)]
pub struts: Struts,
#[knuffel(
child,
unwrap(argument),
default = Some(String::from(
"~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
)))
]
pub screenshot_path: Option<String>,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
pub debug: DebugConfig,
}
// FIXME: Add other devices.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Input {
#[knuffel(child, default)]
pub keyboard: Keyboard,
#[knuffel(child, default)]
pub touchpad: Touchpad,
#[knuffel(child, default)]
pub tablet: Tablet,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
pub struct Keyboard {
#[knuffel(child, default)]
pub xkb: Xkb,
// The defaults were chosen to match wlroots and sway.
#[knuffel(child, unwrap(argument), default = 600)]
pub repeat_delay: u16,
#[knuffel(child, unwrap(argument), default = 25)]
pub repeat_rate: u8,
#[knuffel(child, unwrap(argument), default)]
pub track_layout: TrackLayout,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq, Clone)]
pub struct Xkb {
#[knuffel(child, unwrap(argument), default)]
pub rules: String,
#[knuffel(child, unwrap(argument), default)]
pub model: String,
#[knuffel(child, unwrap(argument))]
pub layout: Option<String>,
#[knuffel(child, unwrap(argument), default)]
pub variant: String,
#[knuffel(child, unwrap(argument))]
pub options: Option<String>,
}
impl Xkb {
pub fn to_xkb_config(&self) -> XkbConfig {
XkbConfig {
rules: &self.rules,
model: &self.model,
layout: self.layout.as_deref().unwrap_or("us"),
variant: &self.variant,
options: self.options.clone(),
}
}
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
pub enum TrackLayout {
/// The layout change is global.
#[default]
Global,
/// The layout change is window local.
Window,
}
// FIXME: Add the rest of the settings.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Touchpad {
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Tablet {
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(child)]
pub off: bool,
#[knuffel(argument)]
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
pub mode: Option<Mode>,
}
impl Default for Output {
fn default() -> Self {
Self {
off: false,
name: String::new(),
scale: 1.,
position: None,
mode: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
#[knuffel(property)]
pub y: i32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
pub refresh: Option<f64>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct SpawnAtStartup {
#[knuffel(arguments)]
pub command: Vec<String>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FocusRing {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = 4)]
pub width: u16,
#[knuffel(child, default = Color::new(127, 200, 255, 255))]
pub active_color: Color,
#[knuffel(child, default = Color::new(80, 80, 80, 255))]
pub inactive_color: Color,
}
impl Default for FocusRing {
fn default() -> Self {
Self {
off: false,
width: 4,
active_color: Color::new(127, 200, 255, 255),
inactive_color: Color::new(80, 80, 80, 255),
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Color {
#[knuffel(argument)]
pub r: u8,
#[knuffel(argument)]
pub g: u8,
#[knuffel(argument)]
pub b: u8,
#[knuffel(argument)]
pub a: u8,
}
impl Color {
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
}
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
}
}
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Cursor {
#[knuffel(child, unwrap(argument), default = String::from("default"))]
pub xcursor_theme: String,
#[knuffel(child, unwrap(argument), default = 24)]
pub xcursor_size: u8,
}
impl Default for Cursor {
fn default() -> Self {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetWidth {
Proportion(#[knuffel(argument)] f64),
Fixed(#[knuffel(argument)] i32),
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Struts {
#[knuffel(child, unwrap(argument), default)]
pub left: u16,
#[knuffel(child, unwrap(argument), default)]
pub right: u16,
#[knuffel(child, unwrap(argument), default)]
pub top: u16,
#[knuffel(child, unwrap(argument), default)]
pub bottom: u16,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Bind {
#[knuffel(node_name)]
pub key: Key,
#[knuffel(children)]
pub actions: Vec<Action>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Key {
pub keysym: Keysym,
pub modifiers: Modifiers,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Modifiers : u8 {
const CTRL = 1;
const SHIFT = 2;
const ALT = 4;
const SUPER = 8;
const COMPOSITOR = 16;
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum Action {
Quit,
#[knuffel(skip)]
ChangeVt(i32),
Suspend,
PowerOffMonitors,
ToggleDebugTint,
Spawn(#[knuffel(arguments)] Vec<String>),
#[knuffel(skip)]
ConfirmScreenshot,
#[knuffel(skip)]
CancelScreenshot,
Screenshot,
ScreenshotScreen,
ScreenshotWindow,
CloseWindow,
FullscreenWindow,
FocusColumnLeft,
FocusColumnRight,
FocusWindowDown,
FocusWindowUp,
FocusWindowOrWorkspaceDown,
FocusWindowOrWorkspaceUp,
MoveColumnLeft,
MoveColumnRight,
MoveWindowDown,
MoveWindowUp,
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] u8),
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
FocusMonitorRight,
FocusMonitorDown,
FocusMonitorUp,
MoveWindowToMonitorLeft,
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
SetFixed(i32),
SetProportion(f64),
AdjustFixed(i32),
AdjustProportion(f64),
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
pub enum LayoutAction {
Next,
Prev,
}
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument), default = 1.)]
pub animation_slowdown: f64,
#[knuffel(child)]
pub dbus_interfaces_in_non_session_instances: bool,
#[knuffel(child)]
pub wait_for_frame_completion_before_queueing: bool,
#[knuffel(child)]
pub enable_color_transformations_capability: bool,
#[knuffel(child)]
pub enable_overlay_planes: bool,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
animation_slowdown: 1.,
dbus_interfaces_in_non_session_instances: false,
wait_for_frame_completion_before_queueing: false,
enable_color_transformations_capability: false,
enable_overlay_planes: false,
}
}
}
impl Config {
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
let path = if let Some(path) = path {
path
} else {
let mut path = ProjectDirs::from("", "", "niri")
.ok_or_else(|| miette!("error retrieving home directory"))?
.config_dir()
.to_owned();
path.push("config.kdl");
path
};
let contents = std::fs::read_to_string(&path)
.into_diagnostic()
.with_context(|| format!("error reading {path:?}"))?;
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
debug!("loaded config from {path:?}");
Ok((config, path))
}
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
knuffel::parse(filename, text)
}
}
impl Default for Config {
fn default() -> Self {
Config::parse(
"default-config.kdl",
include_str!("../resources/default-config.kdl"),
)
.unwrap()
}
}
impl FromStr for Mode {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once('x') else {
return Err(miette!("no 'x' separator found"));
};
let (height, refresh) = match rest.split_once('@') {
Some((height, refresh)) => (height, Some(refresh)),
None => (rest, None),
};
let width = width
.parse()
.into_diagnostic()
.context("error parsing width")?;
let height = height
.parse()
.into_diagnostic()
.context("error parsing height")?;
let refresh = refresh
.map(str::parse)
.transpose()
.into_diagnostic()
.context("error parsing refresh rate")?;
Ok(Self {
width,
height,
refresh,
})
}
}
impl FromStr for Key {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut modifiers = Modifiers::empty();
let mut split = s.split('+');
let key = split.next_back().unwrap();
for part in split {
let part = part.trim();
if part.eq_ignore_ascii_case("mod") {
modifiers |= Modifiers::COMPOSITOR
} else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") {
modifiers |= Modifiers::CTRL;
} else if part.eq_ignore_ascii_case("shift") {
modifiers |= Modifiers::SHIFT;
} else if part.eq_ignore_ascii_case("alt") {
modifiers |= Modifiers::ALT;
} else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") {
modifiers |= Modifiers::SUPER;
} else {
return Err(miette!("invalid modifier: {part}"));
}
}
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
if keysym.raw() == KEY_NoSymbol {
return Err(miette!("invalid key: {key}"));
}
Ok(Key { keysym, modifiers })
}
}
impl FromStr for SizeChange {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err(miette!("trailing characters after '%' are not allowed"));
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err(miette!("value is missing")),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err(miette!("value is missing")),
}
}
}
}
}
#[cfg(test)]
mod tests {
use miette::NarratableReportHandler;
use super::*;
#[track_caller]
fn check(text: &str, expected: Config) {
let _ = miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())));
let parsed = Config::parse("test.kdl", text)
.map_err(miette::Report::new)
.unwrap();
assert_eq!(parsed, expected);
}
#[test]
fn parse() {
check(
r#"
input {
keyboard {
repeat-delay 600
repeat-rate 25
track-layout "window"
xkb {
layout "us,ru"
options "grp:win_space_toggle"
}
}
touchpad {
tap
accel-speed 0.2
}
tablet {
map-to-output "eDP-1"
}
}
output "eDP-1" {
scale 2.0
position x=10 y=20
mode "1920x1080@144"
}
spawn-at-startup "alacritty" "-e" "fish"
focus-ring {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
}
prefer-no-csd
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 16
}
preset-column-widths {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
default-column-width { proportion 0.25; }
gaps 8
struts {
left 1
right 2
top 3
}
screenshot-path "~/Screenshots/screenshot.png"
binds {
Mod+T { spawn "alacritty"; }
Mod+Q { close-window; }
Mod+Shift+H { focus-monitor-left; }
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
Mod+Comma { consume-window-into-column; }
Mod+1 { focus-workspace 1;}
}
debug {
animation-slowdown 2.0
}
"#,
Config {
input: Input {
keyboard: Keyboard {
xkb: Xkb {
layout: Some("us,ru".to_owned()),
options: Some("grp:win_space_toggle".to_owned()),
..Default::default()
},
repeat_delay: 600,
repeat_rate: 25,
track_layout: TrackLayout::Window,
},
touchpad: Touchpad {
tap: true,
natural_scroll: false,
accel_speed: 0.2,
},
tablet: Tablet {
map_to_output: Some("eDP-1".to_owned()),
},
},
outputs: vec![Output {
off: false,
name: "eDP-1".to_owned(),
scale: 2.,
position: Some(Position { x: 10, y: 20 }),
mode: Some(Mode {
width: 1920,
height: 1080,
refresh: Some(144.),
}),
}],
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
}],
focus_ring: FocusRing {
off: false,
width: 5,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
prefer_no_csd: true,
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
],
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
gaps: 8,
struts: Struts {
left: 1,
right: 2,
top: 3,
bottom: 0,
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
binds: Binds(vec![
Bind {
key: Key {
keysym: Keysym::t,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::Spawn(vec!["alacritty".to_owned()])],
},
Bind {
key: Key {
keysym: Keysym::q,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::CloseWindow],
},
Bind {
key: Key {
keysym: Keysym::h,
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
actions: vec![Action::FocusMonitorLeft],
},
Bind {
key: Key {
keysym: Keysym::l,
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
},
actions: vec![Action::MoveWindowToMonitorRight],
},
Bind {
key: Key {
keysym: Keysym::comma,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::ConsumeWindowIntoColumn],
},
Bind {
key: Key {
keysym: Keysym::_1,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::FocusWorkspace(1)],
},
]),
debug: DebugConfig {
animation_slowdown: 2.,
..Default::default()
},
},
);
}
#[test]
fn can_create_default_config() {
let _ = Config::default();
}
#[test]
fn parse_mode() {
assert_eq!(
"2560x1600@165.004".parse::<Mode>().unwrap(),
Mode {
width: 2560,
height: 1600,
refresh: Some(165.004),
},
);
assert_eq!(
"1920x1080".parse::<Mode>().unwrap(),
Mode {
width: 1920,
height: 1080,
refresh: None,
},
);
assert!("1920".parse::<Mode>().is_err());
assert!("1920x".parse::<Mode>().is_err());
assert!("1920x1080@".parse::<Mode>().is_err());
assert!("1920x1080@60Hz".parse::<Mode>().is_err());
}
#[test]
fn parse_size_change() {
assert_eq!(
"10".parse::<SizeChange>().unwrap(),
SizeChange::SetFixed(10),
);
assert_eq!(
"+10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(10),
);
assert_eq!(
"-10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(-10),
);
assert_eq!(
"10%".parse::<SizeChange>().unwrap(),
SizeChange::SetProportion(10.),
);
assert_eq!(
"+10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(10.),
);
assert_eq!(
"-10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(-10.),
);
assert!("-".parse::<SizeChange>().is_err());
assert!("10% ".parse::<SizeChange>().is_err());
}
}
+252
View File
@@ -0,0 +1,252 @@
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::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::animation::Animation;
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> \
to see the errors.";
const PADDING: i32 = 8;
const FONT: &str = "sans 14px";
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 {
Hidden,
Showing(Animation),
Shown(Duration),
Hiding(Animation),
}
pub type ConfigErrorNotificationRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ConfigErrorNotification {
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,
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(self.animation(0., 1.));
}
pub fn hide(&mut self) {
if matches!(self.state, State::Hidden) {
return;
}
self.state = State::Hiding(self.animation(1., 0.));
}
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
match &mut self.state {
State::Hidden => (),
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
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) => {
if target_presentation_time >= *deadline {
self.hide();
}
}
State::Hiding(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Hidden;
}
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
!matches!(self.state, State::Hidden)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<ConfigErrorNotificationRenderElement<R>> {
if matches!(self.state, State::Hidden) {
return None;
}
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, path).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
Some(0.9),
None,
None,
Kind::Unspecified,
)
.ok()?;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let buffer_size = elem
.geometry(output.current_scale().fractional_scale().into())
.size;
let y_range = buffer_size.h + PADDING * 2 * scale;
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
let y = match &self.state {
State::Hidden => unreachable!(),
State::Showing(anim) | State::Hiding(anim) => {
(-buffer_size.h as f64 + anim.value() * y_range as f64).round() as i32
}
State::Shown(_) => PADDING * 2 * scale,
};
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
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::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(&text);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(&text);
cr.set_source_rgb(1., 1., 1.);
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(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_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+5 -12
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<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,
) -> TextureBuffer<GlesTexture> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,19 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
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,
)
.unwrap()
})
.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)
}
}
+8 -2
View File
@@ -4,6 +4,7 @@ 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 +14,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 +26,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>,
@@ -45,9 +48,12 @@ impl DBusServers {
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.connectors());
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
@@ -75,7 +81,7 @@ impl DBusServers {
}
})
.unwrap();
let screen_cast = ScreenCast::new(backend.connectors(), to_niri);
let screen_cast = ScreenCast::new(backend.enabled_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
}
}
+44 -12
View File
@@ -4,13 +4,13 @@ use std::sync::{Arc, Mutex};
use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
pub struct DisplayConfig {
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
#[derive(Serialize, Type)]
@@ -53,18 +53,49 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let monitors: Vec<Monitor> = self
.connectors
let mut monitors: Vec<Monitor> = self
.enabled_outputs
.lock()
.unwrap()
.keys()
.map(|c| Monitor {
names: (c.clone(), String::new(), String::new(), String::new()),
modes: vec![],
properties: HashMap::new(),
.map(|c| {
// Loosely matches the check in Mutter.
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
// FIXME: use proper serial when we have libdisplay-info.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let mut properties = HashMap::new();
if is_laptop_panel {
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
);
}
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
Monitor {
names: (c.clone(), String::new(), String::new(), serial),
modes: vec![],
properties,
}
})
.collect();
// Sort the built-in monitor first, then by connector name.
monitors.sort_unstable_by(|a, b| {
let a_is_builtin = a.properties.contains_key("display-name");
let b_is_builtin = b.properties.contains_key("display-name");
a_is_builtin
.cmp(&b_is_builtin)
.reverse()
.then_with(|| a.names.0.cmp(&b.names.0))
});
let logical_monitors = monitors
.iter()
.map(|m| LogicalMonitor {
@@ -81,12 +112,13 @@ 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 {
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { connectors }
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { enabled_outputs }
}
}
+30 -9
View File
@@ -7,23 +7,26 @@ 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 {
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
}
#[derive(Clone)]
pub struct Session {
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
}
@@ -52,6 +55,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,
@@ -62,6 +72,7 @@ pub enum ScreenCastToNiri {
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -82,7 +93,11 @@ impl ScreenCast {
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
let path = OwnedObjectPath::try_from(path).unwrap();
let session = Session::new(session_id, self.connectors.clone(), self.to_niri.clone());
let session = Session::new(
session_id,
self.enabled_outputs.clone(),
self.to_niri.clone(),
);
match server.at(&path, session.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -149,7 +164,7 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else {
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
@@ -188,15 +203,21 @@ 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 {
pub fn new(
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
connectors,
enabled_outputs,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
@@ -221,12 +242,12 @@ impl Start for ScreenCast {
impl Session {
pub fn new(
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
connectors,
enabled_outputs,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
}
+7 -3
View File
@@ -25,9 +25,13 @@ impl ServiceChannel {
}
let (sock1, sock2) = UnixStream::pair().unwrap();
self.display
.insert_client(sock2, Arc::new(ClientState::default()))
.unwrap();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
restricted: false,
});
self.display.insert_client(sock2, data).unwrap();
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::cell::RefCell;
use std::collections::HashMap;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{Alignment, FontDescription};
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
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.";
const PADDING: i32 = 16;
const FONT: &str = "sans 14px";
const BORDER: i32 = 8;
pub struct ExitConfirmDialog {
is_open: bool,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
}
pub type ExitConfirmDialogRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ExitConfirmDialog {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
is_open: false,
buffers: RefCell::new(HashMap::from([(1, Some(render(1)?))])),
})
}
pub fn show(&mut self) -> bool {
if !self.is_open {
self.is_open = true;
true
} else {
false
}
}
pub fn hide(&mut self) -> bool {
if self.is_open {
self.is_open = false;
true
} else {
false
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<ExitConfirmDialogRenderElement<R>> {
if !self.is_open {
return None;
}
let scale = output.current_scale().integer_scale();
let mut buffers = self.buffers.borrow_mut();
let fallback = buffers[&1].clone().unwrap();
let buffer = buffers.entry(scale).or_insert_with(|| render(scale).ok());
let buffer = buffer.as_ref().unwrap_or(&fallback);
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
None,
None,
None,
Kind::Unspecified,
)
.ok()?;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let buffer_size = elem
.geometry(output.current_scale().fractional_scale().into())
.size;
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
let y = (output_size.h / 2 - buffer_size.h / 2).max(0);
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("exit_confirm_dialog::render");
let padding = PADDING * scale;
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::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
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::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_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+34 -8
View File
@@ -18,6 +18,7 @@ use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell;
use crate::niri::{ClientState, State};
use crate::utils::clone2;
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -81,6 +82,7 @@ impl CompositorHandler for State {
let _span = tracy_client::span!("CompositorHandler::commit");
on_commit_buffer_handler::<Self>(surface);
self.backend.early_import(surface);
if is_sync_subsurface(surface) {
return;
@@ -95,15 +97,34 @@ 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();
window.on_commit();
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
{
let parent = window
.toplevel()
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(win, _)| win.clone());
let win = window.clone();
// Open dialogs immediately to the right of their parent window.
let output = if let Some(p) = parent {
self.niri.layout.add_window_right_of(&p, win, None, false)
} else {
self.niri.layout.add_window(win, None, false)
};
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
self.niri.queue_redraw(output);
}
return;
@@ -116,13 +137,18 @@ impl CompositorHandler for State {
}
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some((window, output)) = self.niri.layout.find_window_and_output(surface) {
// This is a commit of a previously-mapped toplevel.
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
let (window, output) = clone2(win_out);
window.on_commit();
// 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.
@@ -147,7 +173,7 @@ impl CompositorHandler for State {
// This is a commit of a non-root or a non-toplevel root.
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((window, output)) = root_window_output {
if let Some((window, output)) = root_window_output.map(clone2) {
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.queue_redraw(output);
@@ -158,7 +184,7 @@ impl CompositorHandler for State {
self.popups_handle_commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(output);
self.niri.queue_redraw(output.clone());
}
}
+187 -19
View File
@@ -9,11 +9,13 @@ use std::sync::Arc;
use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::ImportDma;
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::input;
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;
@@ -21,8 +23,17 @@ 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,
};
use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
@@ -37,14 +48,19 @@ 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_session_lock, delegate_tablet_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_virtual_keyboard_manager,
};
use crate::layout::output_size;
use crate::niri::State;
use crate::delegate_foreign_toplevel;
use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::utils::output_size;
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
@@ -71,6 +87,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);
@@ -187,13 +216,18 @@ 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);
impl DmabufHandler for State {
fn dmabuf_state(&mut self) -> &mut DmabufState {
self.backend.tty().dmabuf_state()
&mut self.niri.dmabuf_state
}
fn dmabuf_imported(
@@ -202,17 +236,10 @@ impl DmabufHandler for State {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
let renderer = self.backend.renderer().expect(
"the dmabuf global must be created and destroyed together with the output device",
);
match renderer.import_dmabuf(&dmabuf, None) {
Ok(_texture) => {
let _ = notifier.successful::<State>();
}
Err(err) => {
debug!("error importing dmabuf: {err:?}");
notifier.failed();
}
if self.backend.import_dmabuf(&dmabuf) {
let _ = notifier.successful::<State>();
} else {
notifier.failed();
}
}
}
@@ -256,3 +283,144 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
});
surface.send_configure();
}
impl SecurityContextHandler for State {
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
self.niri
.event_loop
.insert_source(source, move |client, _, state| {
let config = state.niri.config.borrow();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
restricted: true,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
error!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
})
.unwrap();
}
}
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().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()
.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 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);
+143 -28
View File
@@ -1,7 +1,9 @@
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKind, PopupManager, Window, WindowSurfaceType,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
use smithay::input::pointer::Focus;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
@@ -11,7 +13,9 @@ 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,
@@ -19,7 +23,8 @@ use smithay::wayland::shell::xdg::{
};
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
use crate::niri::State;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -89,8 +94,102 @@ impl XdgShellHandler for State {
surface.send_repositioned(token);
}
fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {
// FIXME popup grabs
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;
};
// We need to hand out the grab in a way consistent with what update_keyboard_focus()
// thinks the current focus is, otherwise it will desync and cause weird issues with
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
.previous_serial()
.map_or(true, |s| keyboard.has_grab(s)));
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
@@ -115,16 +214,15 @@ impl XdgShellHandler for State {
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
// NOTE: This is only one part of the solution. We can set the
// location and configure size here, but the surface should be rendered fullscreen
// independently from its buffer size
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if requested_output != current_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
@@ -132,6 +230,13 @@ impl XdgShellHandler for State {
}
self.niri.layout.set_fullscreen(&window, true);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.view_size());
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
}
}
@@ -146,7 +251,15 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.new_window_size());
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
}
}
@@ -166,7 +279,7 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out else {
let Some((window, output)) = win_out.map(clone2) else {
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
// involving laptop going to sleep and resuming.
error!("toplevel missing from both unmapped_windows and layout");
@@ -179,7 +292,7 @@ impl XdgShellHandler for State {
fn popup_destroyed(&mut self, surface: PopupSurface) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output);
self.niri.queue_redraw(output.clone());
}
}
}
@@ -188,40 +301,42 @@ delegate_xdg_shell!(State);
impl XdgDecorationHandler for State {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
}
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
// Set whatever the client wants, rather than our preferred mode. This especially matters
// for SDL2 which has a bug where forcing a different (client-side) decoration mode during
// their window creation sequence would leave the window permanently hidden.
//
// https://github.com/libsdl-org/SDL/issues/8173
//
// The bug has been fixed, but there's a ton of apps which will use the buggy version for a
// long while...
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(mode);
});
// Only send configure if it's non-initial.
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
// Only send configure if it's non-initial.
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
}
@@ -288,7 +403,7 @@ impl State {
}
}
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<Output> {
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<&Output> {
let root = find_popup_root_surface(popup).ok()?;
self.niri.output_for_root(&root)
}
@@ -304,7 +419,7 @@ impl State {
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &window, &output);
self.unconstrain_window_popup(popup, window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
+451
View File
@@ -0,0 +1,451 @@
use std::cell::RefCell;
use std::cmp::max;
use std::collections::HashMap;
use std::iter::zip;
use std::rc::Rc;
use niri_config::{Action, Config, Key, Modifiers};
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::input::keyboard::xkb::keysym_get_name;
use smithay::output::{Output, WeakOutput};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::{Physical, Size, Transform};
use crate::input::CompositorMod;
use crate::render_helpers::renderer::NiriRenderer;
const PADDING: i32 = 8;
const MARGIN: i32 = PADDING * 2;
const FONT: &str = "sans 14px";
const BORDER: i32 = 4;
const LINE_INTERVAL: i32 = 2;
const TITLE: &str = "Important Hotkeys";
pub struct HotkeyOverlay {
is_open: bool,
config: Rc<RefCell<Config>>,
comp_mod: CompositorMod,
buffers: RefCell<HashMap<WeakOutput, RenderedOverlay>>,
}
pub struct RenderedOverlay {
buffer: Option<MemoryRenderBuffer>,
size: Size<i32, Physical>,
scale: i32,
}
pub type HotkeyOverlayRenderElement<R> = RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl HotkeyOverlay {
pub fn new(config: Rc<RefCell<Config>>, comp_mod: CompositorMod) -> Self {
Self {
is_open: false,
config,
comp_mod,
buffers: RefCell::new(HashMap::new()),
}
}
pub fn show(&mut self) -> bool {
if !self.is_open {
self.is_open = true;
true
} else {
false
}
}
pub fn hide(&mut self) -> bool {
if self.is_open {
self.is_open = false;
true
} else {
false
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn on_hotkey_config_updated(&mut self) {
self.buffers.borrow_mut().clear();
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<HotkeyOverlayRenderElement<R>> {
if !self.is_open {
return None;
}
let scale = output.current_scale().integer_scale();
let margin = MARGIN * scale;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let mut buffers = self.buffers.borrow_mut();
buffers.retain(|output, _| output.upgrade().is_some());
// FIXME: should probably use the working area rather than view size.
let weak = output.downgrade();
if let Some(rendered) = buffers.get(&weak) {
if rendered.scale != scale {
buffers.remove(&weak);
}
}
let rendered = buffers.entry(weak).or_insert_with(|| {
render(&self.config.borrow(), self.comp_mod, scale).unwrap_or_else(|_| {
// This can go negative but whatever, as long as there's no rerender loop.
let mut size = output_size;
size.w -= margin * 2;
size.h -= margin * 2;
RenderedOverlay {
buffer: None,
size,
scale,
}
})
});
let buffer = rendered.buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
Some(0.9),
None,
None,
Kind::Unspecified,
)
.ok()?;
let x = (output_size.w / 2 - rendered.size.w / 2).max(0);
let y = (output_size.h / 2 - rendered.size.h / 2).max(0);
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Result<RenderedOverlay> {
let _span = tracy_client::span!("hotkey_overlay::render");
// let margin = MARGIN * scale;
let padding = PADDING * scale;
let line_interval = LINE_INTERVAL * scale;
// FIXME: if it doesn't fit, try splitting in two columns or something.
// let mut target_size = output_size;
// target_size.w -= margin * 2;
// target_size.h -= margin * 2;
// anyhow::ensure!(target_size.w > 0 && target_size.h > 0);
let binds = &config.binds.0;
// Collect actions that we want to show.
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.actions.first() == Some(&Action::Quit(false)))
{
actions.push(&Action::Quit(false));
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::Quit(true)))
{
actions.push(&Action::Quit(true));
} else {
actions.push(&Action::Quit(false));
}
actions.extend(&[
&Action::CloseWindow,
&Action::FocusColumnLeft,
&Action::FocusColumnRight,
&Action::MoveColumnLeft,
&Action::MoveColumnRight,
&Action::FocusWorkspaceDown,
&Action::FocusWorkspaceUp,
]);
// 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))
{
actions.push(&Action::MoveColumnToWorkspaceDown);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
{
actions.push(&Action::MoveWindowToWorkspaceDown);
} else {
actions.push(&Action::MoveColumnToWorkspaceDown);
}
// Same for -up.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
{
actions.push(&Action::MoveColumnToWorkspaceUp);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
{
actions.push(&Action::MoveWindowToWorkspaceUp);
} else {
actions.push(&Action::MoveColumnToWorkspaceUp);
}
actions.extend(&[
&Action::SwitchPresetColumnWidth,
&Action::MaximizeColumn,
&Action::ConsumeWindowIntoColumn,
&Action::ExpelWindowFromColumn,
]);
// Screenshot is not as important, can omit if not bound.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
{
actions.push(&Action::Screenshot);
}
// Add the spawn actions.
let mut spawn_actions = Vec::new();
for bind in binds.iter().filter(|bind| {
matches!(bind.actions.first(), Some(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.actions.first().unwrap();
// 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()
.map(|action| {
let key = config
.binds
.0
.iter()
.find(|bind| bind.actions.first() == Some(action))
.map(|bind| key_name(comp_mod, &bind.key))
.unwrap_or_else(|| String::from("(not bound)"));
(format!(" {key} "), action_name(action))
})
.collect::<Vec<_>>();
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::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
let bold = AttrList::new();
bold.insert(AttrInt::new_weight(Weight::Bold));
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
let title_size = layout.pixel_size();
let attrs = AttrList::new();
attrs.insert(AttrString::new_family("Monospace"));
attrs.insert(AttrColor::new_background(12000, 12000, 12000));
layout.set_attributes(Some(&attrs));
let key_sizes = strings
.iter()
.map(|(key, _)| {
layout.set_text(key);
layout.pixel_size()
})
.collect::<Vec<_>>();
layout.set_attributes(None);
let action_sizes = strings
.iter()
.map(|(_, action)| {
layout.set_markup(action);
layout.pixel_size()
})
.collect::<Vec<_>>();
let key_width = key_sizes.iter().map(|(w, _)| w).max().unwrap();
let action_width = action_sizes.iter().map(|(w, _)| w).max().unwrap();
let mut width = key_width + padding + action_width;
let mut height = zip(&key_sizes, &action_sizes)
.map(|((_, key_h), (_, act_h))| max(key_h, act_h))
.sum::<i32>()
+ (key_sizes.len() - 1) as i32 * line_interval
+ title_size.1
+ padding;
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
cr.set_source_rgb(1., 1., 1.);
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
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::functions::show_layout(&cr, &layout);
cr.rel_move_to((key_width + padding).into(), 0.);
layout.set_attributes(None);
layout.set_markup(action);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to(
(-(key_width + padding)).into(),
(max(key_h, act_h) + line_interval).into(),
);
}
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(0.5, 0.8, 1.0);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(RenderedOverlay {
buffer: Some(buffer),
size: Size::from((width, height)),
scale,
})
}
fn action_name(action: &Action) -> String {
match action {
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"),
Action::FocusColumnRight => String::from("Focus Column to the Right"),
Action::MoveColumnLeft => String::from("Move Column Left"),
Action::MoveColumnRight => String::from("Move Column Right"),
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
Action::MaximizeColumn => String::from("Maximize Column"),
Action::ConsumeWindowIntoColumn => String::from("Consume Window Into Column"),
Action::ExpelWindowFromColumn => String::from("Expel Window From Column"),
Action::Screenshot => String::from("Take a Screenshot"),
Action::Spawn(args) => format!(
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
args.first().unwrap_or(&String::new())
),
_ => String::from("FIXME: Unknown"),
}
}
fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
let mut name = String::new();
let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR);
if key.modifiers.contains(Modifiers::SUPER)
|| (has_comp_mod && comp_mod == CompositorMod::Super)
{
name.push_str("Super + ");
}
if key.modifiers.contains(Modifiers::ALT) || (has_comp_mod && comp_mod == CompositorMod::Alt) {
name.push_str("Alt + ");
}
if key.modifiers.contains(Modifiers::SHIFT) {
name.push_str("Shift + ");
}
if key.modifiers.contains(Modifiers::CTRL) {
name.push_str("Ctrl + ");
}
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
name
}
fn prettify_keysym_name(name: &str) -> String {
let name = match name {
"slash" => "/",
"comma" => ",",
"period" => ".",
"minus" => "-",
"equal" => "=",
"grave" => "`",
"Next" => "Page Down",
"Prior" => "Page Up",
"Print" => "PrtSc",
"Return" => "Enter",
_ => name,
};
if name.len() == 1 && name.is_ascii() {
name.to_ascii_uppercase()
} else {
name.into()
}
}
+515 -45
View File
@@ -1,6 +1,8 @@
use std::any::Any;
use std::collections::HashSet;
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 _,
@@ -20,7 +22,6 @@ 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::config::{Action, Binds, LayoutAction, Modifiers};
use crate::niri::State;
use crate::screenshot_ui::ScreenshotUi;
use crate::utils::{center, get_monotonic_time, spawn};
@@ -49,11 +50,36 @@ 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 =
self.niri.hotkey_overlay.is_open() && should_hide_hotkey_overlay(&event);
let hide_exit_confirm_dialog = self
.niri
.exit_confirm_dialog
.as_ref()
.map_or(false, |d| d.is_open())
&& should_hide_exit_confirm_dialog(&event);
use InputEvent::*;
match event {
DeviceAdded { device } => self.on_device_added(device),
@@ -80,8 +106,21 @@ impl State {
TouchUp { .. } => (),
TouchCancel { .. } => (),
TouchFrame { .. } => (),
SwitchToggle { .. } => (),
Special(_) => (),
}
// Do this last so that screenshot still gets it.
// FIXME: do this in a less cursed fashion somehow.
if hide_hotkey_overlay && self.niri.hotkey_overlay.hide() {
self.niri.queue_redraw_all();
}
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
if hide_exit_confirm_dialog && dialog.hide() {
self.niri.queue_redraw_all();
}
}
}
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
@@ -89,14 +128,7 @@ impl State {
match event {
InputEvent::DeviceAdded { device } => {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
if is_touchpad {
let c = &self.niri.config.borrow().input.touchpad;
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
}
self.niri.devices.insert(device.clone());
if device.has_capability(input::DeviceCapability::TabletTool) {
match device.size() {
@@ -110,9 +142,23 @@ 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());
}
}
apply_libinput_settings(&self.niri.config.borrow().input, device);
}
InputEvent::DeviceRemoved { device } => {
self.niri.tablets.remove(device);
self.niri.devices.remove(device);
}
_ => (),
}
@@ -199,6 +245,14 @@ impl State {
let key_code = event.key_code();
let modified = keysym.modified_sym();
let raw = keysym.raw_latin_sym_or_raw_current_sym();
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();
}
}
should_intercept_key(
&mut this.niri.suppressed_keys,
bindings,
@@ -209,6 +263,7 @@ impl State {
pressed,
*mods,
&this.niri.screenshot_ui,
this.niri.config.borrow().input.disable_power_key_handling,
)
},
) else {
@@ -220,18 +275,31 @@ 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;
}
match action {
Action::Quit => {
info!("quitting because quit bind was pressed");
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;
}
}
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 => {
@@ -240,10 +308,11 @@ 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();
self.niri.queue_redraw_all();
}
Action::Spawn(command) => {
spawn(command);
@@ -251,15 +320,15 @@ impl State {
Action::ScreenshotScreen => {
let active = self.niri.layout.active_output().cloned();
if let Some(active) = active {
if let Some(renderer) = self.backend.renderer() {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot(renderer, &active) {
warn!("error taking screenshot: {err:?}");
}
}
});
}
}
Action::ConfirmScreenshot => {
if let Some(renderer) = self.backend.renderer() {
self.backend.with_primary_renderer(|renderer| {
match self.niri.screenshot_ui.capture(renderer) {
Ok((size, pixels)) => {
if let Err(err) = self.niri.save_screenshot(size, pixels) {
@@ -270,7 +339,7 @@ impl State {
warn!("error capturing screenshot: {err:?}");
}
}
}
});
self.niri.screenshot_ui.close();
self.niri
@@ -286,18 +355,18 @@ impl State {
self.niri.queue_redraw_all();
}
Action::Screenshot => {
if let Some(renderer) = self.backend.renderer() {
self.backend.with_primary_renderer(|renderer| {
self.niri.open_screenshot_ui(renderer);
}
});
}
Action::ScreenshotWindow => {
let active = self.niri.layout.active_window();
if let Some((window, output)) = active {
if let Some(renderer) = self.backend.renderer() {
if let Err(err) = self.niri.screenshot_window(renderer, &output, &window) {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot_window(renderer, output, window) {
warn!("error taking screenshot: {err:?}");
}
}
});
}
}
Action::CloseWindow => {
@@ -309,14 +378,16 @@ impl State {
let focus = self.niri.layout.focus().cloned();
if let Some(window) = focus {
self.niri.layout.toggle_fullscreen(&window);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::SwitchLayout(action) => {
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(),
},
);
}
@@ -330,6 +401,16 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToFirst => {
self.niri.layout.move_column_to_first();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToLast => {
self.niri.layout.move_column_to_last();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowDown => {
self.niri.layout.move_down();
// FIXME: granular
@@ -350,17 +431,45 @@ 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
self.niri.queue_redraw_all();
}
Action::FocusColumnRight => {
self.niri.layout.focus_right();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnFirst => {
self.niri.layout.focus_column_first();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLast => {
self.niri.layout.focus_column_last();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDown => {
self.niri.layout.focus_down();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUp => {
self.niri.layout.focus_up();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
@@ -388,6 +497,22 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspaceDown => {
self.niri.layout.move_column_to_workspace_down();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspaceUp => {
self.niri.layout.move_column_to_workspace_up();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.move_column_to_workspace(idx);
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspaceDown => {
self.niri.layout.switch_workspace_down();
// FIXME: granular
@@ -483,12 +608,65 @@ impl State {
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.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_column_to_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.move_cursor_to_output(&output);
}
}
Action::MoveColumnToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_column_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::SetColumnWidth(change) => {
self.niri.layout.set_column_width(change);
}
Action::SetWindowHeight(change) => {
self.niri.layout.set_window_height(change);
}
Action::ShowHotkeyOverlay => {
if self.niri.hotkey_overlay.show() {
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);
}
}
}
}
@@ -736,12 +914,18 @@ impl State {
let button_state = event.state();
if ButtonState::Pressed == button_state && !pointer.is_grabbed() {
if ButtonState::Pressed == button_state {
if let Some(window) = self.niri.window_under_cursor() {
let window = window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = self.niri.output_under_cursor() {
self.niri.layout.activate_output(&output);
// FIXME: granular.
self.niri.queue_redraw_all();
}
};
@@ -800,12 +984,16 @@ impl State {
let mut frame = AxisFrame::new(event.time_msec()).source(source);
if horizontal_amount != 0.0 {
frame = frame
.relative_direction(Axis::Horizontal, event.relative_direction(Axis::Horizontal));
frame = frame.value(Axis::Horizontal, horizontal_amount);
if let Some(discrete) = horizontal_amount_discrete {
frame = frame.v120(Axis::Horizontal, discrete as i32);
}
}
if vertical_amount != 0.0 {
frame =
frame.relative_direction(Axis::Vertical, event.relative_direction(Axis::Vertical));
frame = frame.value(Axis::Vertical, vertical_amount);
if let Some(discrete) = vertical_amount_discrete {
frame = frame.v120(Axis::Vertical, discrete as i32);
@@ -1002,11 +1190,20 @@ impl State {
);
}
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
let res = self
.niri
.layout
.workspace_switch_gesture_update(event.delta_y());
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent)
where
I::Device: 'static,
{
let mut delta_y = event.delta_y();
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
delta_y = -delta_y;
}
}
let res = self.niri.layout.workspace_switch_gesture_update(delta_y);
if let Some(output) = res {
if let Some(output) = output {
self.niri.queue_redraw(output);
@@ -1165,6 +1362,7 @@ fn should_intercept_key(
pressed: bool,
mods: ModifiersState,
screenshot_ui: &ScreenshotUi,
disable_power_key_handling: bool,
) -> FilterResult<Option<Action>> {
// Actions are only triggered on presses, release of the key
// shouldn't try to intercept anything unless we have marked
@@ -1173,7 +1371,14 @@ fn should_intercept_key(
return FilterResult::Forward;
}
let mut final_action = action(bindings, comp_mod, modified, raw, mods);
let mut final_action = action(
bindings,
comp_mod,
modified,
raw,
mods,
disable_power_key_handling,
);
// Allow only a subset of compositor actions while the screenshot UI is open, since the user
// cannot see the screen.
@@ -1210,6 +1415,7 @@ fn action(
modified: Keysym,
raw: Option<Keysym>,
mods: ModifiersState,
disable_power_key_handling: bool,
) -> Option<Action> {
use keysyms::*;
@@ -1220,10 +1426,19 @@ fn action(
let vt = (modified - KEY_XF86Switch_VT_1 + 1) as i32;
return Some(Action::ChangeVt(vt));
}
KEY_XF86PowerOff => return Some(Action::Suspend),
KEY_XF86PowerOff if !disable_power_key_handling => return Some(Action::Suspend),
_ => (),
}
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 {
@@ -1239,26 +1454,29 @@ 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 Some(raw) = raw else {
return None;
};
let raw = raw?;
for bind in &bindings.0 {
if bind.key.keysym != raw {
continue;
}
if bind.key.modifiers | comp_mod == modifiers {
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 bind.actions.first().cloned();
}
}
@@ -1287,10 +1505,47 @@ 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::GestureSwipeBegin { .. }
| InputEvent::GesturePinchBegin { .. }
| InputEvent::TouchDown { .. }
| InputEvent::TouchMotion { .. }
| InputEvent::TabletToolTip { .. }
| InputEvent::TabletToolButton { .. } => true,
_ => false,
}
}
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::GestureSwipeBegin { .. }
| InputEvent::GesturePinchBegin { .. }
| InputEvent::TouchDown { .. }
| InputEvent::TouchMotion { .. }
| InputEvent::TabletToolTip { .. }
| InputEvent::TabletToolButton { .. } => true,
_ => false,
}
}
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
@@ -1301,14 +1556,71 @@ 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
)
}
pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
if is_touchpad {
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);
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);
}
if let Some(tap_button_map) = c.tap_button_map {
let _ = device.config_tap_set_button_map(tap_button_map.into());
} else if let Some(default) = device.config_tap_default_button_map() {
let _ = device.config_tap_set_button_map(default);
}
}
// This is how Mutter tells apart mice.
let mut is_trackball = false;
let mut is_trackpoint = false;
if let Some(udev_device) = unsafe { device.udev_device() } {
if udev_device.property_value("ID_INPUT_TRACKBALL").is_some() {
is_trackball = true;
}
if udev_device
.property_value("ID_INPUT_POINTINGSTICK")
.is_some()
{
is_trackpoint = true;
}
}
let is_mouse = device.has_capability(input::DeviceCapability::Pointer)
&& !is_touchpad
&& !is_trackball
&& !is_trackpoint;
if is_mouse {
let c = &config.mouse;
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 super::*;
use crate::config::{Action, Bind, Binds, Key, Modifiers};
#[test]
fn bindings_suppress_keys() {
@@ -1325,6 +1637,7 @@ mod tests {
let mut suppressed_keys = HashSet::new();
let screenshot_ui = ScreenshotUi::new();
let disable_power_key_handling = false;
// The key_code we pick is arbitrary, the only thing
// that matters is that they are different between cases.
@@ -1341,6 +1654,7 @@ mod tests {
pressed,
mods,
&screenshot_ui,
disable_power_key_handling,
)
};
@@ -1356,6 +1670,7 @@ mod tests {
pressed,
mods,
&screenshot_ui,
disable_power_key_handling,
)
};
@@ -1428,4 +1743,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,
},
actions: vec![Action::CloseWindow],
},
Bind {
key: Key {
keysym: Keysym::h,
modifiers: Modifiers::SUPER,
},
actions: vec![Action::FocusColumnLeft],
},
Bind {
key: Key {
keysym: Keysym::j,
modifiers: Modifiers::empty(),
},
actions: vec![Action::FocusWindowDown],
},
Bind {
key: Key {
keysym: Keysym::k,
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
},
actions: vec![Action::FocusWindowUp],
},
Bind {
key: Key {
keysym: Keysym::l,
modifiers: Modifiers::SUPER | Modifiers::ALT,
},
actions: vec![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,
);
}
}
+115
View File
@@ -0,0 +1,115 @@
use std::env;
use std::io::{Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use anyhow::{anyhow, bail, Context};
use niri_ipc::{Mode, Output, Reply, Request, Response};
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(|| {
format!(
"{} is not set, are you running this within niri?",
niri_ipc::SOCKET_PATH_ENV
)
})?;
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match &msg {
Msg::Outputs => Request::Outputs,
Msg::Action { action } => Request::Action(action.clone()),
};
let mut buf = serde_json::to_vec(&request).unwrap();
stream
.write_all(&buf)
.context("error writing IPC request")?;
stream
.shutdown(Shutdown::Write)
.context("error closing IPC stream for writing")?;
buf.clear();
stream
.read_to_end(&mut buf)
.context("error reading IPC response")?;
let 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 => {
let Response::Outputs(outputs) = response else {
bail!("unexpected response: expected Outputs, got {response:?}");
};
if json {
let output =
serde_json::to_string(&outputs).context("error formatting response")?;
println!("{output}");
return Ok(());
}
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
for (connector, output) in outputs.into_iter() {
let Output {
name,
make,
model,
physical_size,
modes,
current_mode,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
} else {
println!(" Disabled");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
println!(" Physical size: unknown");
}
println!(" Available modes:");
for mode in modes {
let Mode {
width,
height,
refresh_rate,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" {width}x{height}@{refresh:.3}");
}
println!();
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
}
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod client;
pub mod server;
+142
View File
@@ -0,0 +1,142 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::rc::Rc;
use std::{env, io, process};
use anyhow::Context;
use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{Request, Response};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use crate::niri::State;
pub struct IpcServer {
pub socket_path: PathBuf,
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
}
impl IpcServer {
pub fn start(
event_loop: &LoopHandle<'static, State>,
wayland_socket_name: &str,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("Ipc::start");
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
Ok(PostAction::Continue)
})
.unwrap();
Ok(Self { socket_path })
}
}
impl Drop for IpcServer {
fn drop(&mut self) {
let _ = unlink(&self.socket_path);
}
}
fn socket_dir() -> PathBuf {
BaseDirs::new()
.as_ref()
.and_then(|x| x.runtime_dir())
.map(|x| x.to_owned())
.unwrap_or_else(env::temp_dir)
}
fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
let _span = tracy_client::span!("on_new_ipc_client");
trace!("new IPC client connected");
let stream = match state.niri.event_loop.adapt_io(stream) {
Ok(stream) => stream,
Err(err) => {
warn!("error making IPC stream async: {err:?}");
return;
}
};
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
ipc_outputs: state.backend.ipc_outputs(),
};
let future = async move {
if let Err(err) = handle_client(ctx, stream).await {
warn!("error handling IPC client: {err:?}");
}
};
if let Err(err) = state.niri.scheduler.schedule(future) {
warn!("error scheduling IPC stream future: {err:?}");
}
}
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
// Read a single line to allow extensibility in the future to keep reading.
BufReader::new(read)
.read_line(&mut buf)
.await
.context("error reading request")?;
let 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
}
};
Ok(response)
}
-4025
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{self, Color};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Scale, Size};
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
is_off: bool,
is_border: bool,
width: i32,
active_color: Color,
inactive_color: Color,
}
pub type FocusRingRenderElement = SolidColorRenderElement;
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
is_off: config.off,
is_border: false,
width: config.width.into(),
active_color: config.active_color,
inactive_color: config.inactive_color,
}
}
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;
}
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));
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));
} 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.is_border = is_border;
}
pub fn set_active(&mut self, is_active: bool) {
let color = if is_active {
self.active_color.into()
} else {
self.inactive_color.into()
};
for buf in &mut self.buffers {
buf.set_color(color);
}
}
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.is_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,
);
rv.push(elem);
};
if self.is_border {
for (buf, loc) in zip(&self.buffers, self.locations) {
push(buf, loc);
}
} else {
push(&self.buffers[0], self.locations[0]);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.width
}
pub fn is_off(&self) -> bool {
self.is_off
}
}
+2876
View File
File diff suppressed because it is too large Load Diff
+691
View File
@@ -0,0 +1,691 @@
use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::output_size;
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
pub output: Output,
// Must always contain at least one.
pub workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
pub options: Rc<Options>,
}
#[derive(Debug)]
pub enum WorkspaceSwitch {
Animation(Animation),
Gesture(WorkspaceSwitchGesture),
}
#[derive(Debug)]
pub struct WorkspaceSwitchGesture {
/// Index of the workspace where the gesture was started.
pub center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
}
pub type MonitorRenderElement<R> =
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.value(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
#[must_use]
fn is_animation(&self) -> bool {
matches!(self, Self::Animation(..))
}
}
impl<W: LayoutElement> Monitor<W> {
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
Self {
output,
workspaces,
active_workspace_idx: 0,
workspace_switch: None,
options,
}
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
}
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
current_idx,
idx as f64,
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
}
pub fn add_window(
&mut self,
workspace_idx: usize,
window: W,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window(window, activate, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
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];
workspace.add_column(column, activate);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
for idx in (0..self.workspaces.len() - 1).rev() {
if self.active_workspace_idx == idx {
continue;
}
if !self.workspaces[idx].has_windows() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
}
}
}
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}
pub fn move_right(&mut self) {
self.active_workspace().move_right();
}
pub fn move_column_to_first(&mut self) {
self.active_workspace().move_column_to_first();
}
pub fn move_column_to_last(&mut self) {
self.active_workspace().move_column_to_last();
}
pub fn move_down(&mut self) {
self.active_workspace().move_down();
}
pub fn move_up(&mut self) {
self.active_workspace().move_up();
}
pub fn move_down_or_to_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let column = &mut workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.move_to_workspace_down();
} else {
workspace.move_down();
}
}
pub fn move_up_or_to_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.move_to_workspace_up();
} else {
workspace.move_up();
}
}
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();
}
pub fn focus_right(&mut self) {
self.active_workspace().focus_right();
}
pub fn focus_column_first(&mut self) {
self.active_workspace().focus_column_first();
}
pub fn focus_column_last(&mut self) {
self.active_workspace().focus_column_last();
}
pub fn focus_down(&mut self) {
self.active_workspace().focus_down();
}
pub fn focus_up(&mut self) {
self.active_workspace().focus_up();
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_down();
} else {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.switch_workspace_down();
} else {
workspace.focus_down();
}
}
}
pub fn focus_window_or_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.switch_workspace_up();
} else {
workspace.focus_up();
}
}
}
pub fn move_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = source_workspace_idx.saturating_sub(1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
}
pub fn move_to_workspace_down(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
}
pub fn move_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn move_column_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = source_workspace_idx.saturating_sub(1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace_down(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn switch_workspace_up(&mut self) {
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
}
pub fn switch_workspace_down(&mut self) {
self.activate_workspace(min(
self.active_workspace_idx + 1,
self.workspaces.len() - 1,
));
}
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn consume_into_column(&mut self) {
self.active_workspace().consume_into_column();
}
pub fn expel_from_column(&mut self) {
self.active_workspace().expel_from_column();
}
pub fn center_column(&mut self) {
self.active_workspace().center_column();
}
pub fn focus(&self) -> Option<&W> {
let workspace = &self.workspaces[self.active_workspace_idx];
if !workspace.has_windows() {
return None;
}
let column = &workspace.columns[workspace.active_column_idx];
Some(column.tiles[column.active_tile_idx].window())
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
}
}
for ws in &mut self.workspaces {
ws.advance_animations(current_time, is_active);
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
}
pub fn are_transitions_ongoing(&self) -> bool {
self.workspace_switch.is_some()
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
}
pub fn update_config(&mut self, options: Rc<Options>) {
for ws in &mut self.workspaces {
ws.update_config(options.clone());
}
if self.options.struts != options.struts {
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(view_size, working_area);
}
}
self.options = options;
}
pub fn toggle_width(&mut self) {
self.active_workspace().toggle_width();
}
pub fn toggle_full_width(&mut self) {
self.active_workspace().toggle_full_width();
}
pub fn set_column_width(&mut self, change: SizeChange) {
self.active_workspace().set_column_width(change);
}
pub fn set_window_height(&mut self, change: SizeChange) {
self.active_workspace().set_window_height(change);
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
return;
}
self.workspaces.swap(self.active_workspace_idx, new_idx);
if new_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn move_workspace_up(&mut self) {
let new_idx = self.active_workspace_idx.saturating_sub(1);
if new_idx == self.active_workspace_idx {
return;
}
self.workspaces.swap(self.active_workspace_idx, new_idx);
if self.active_workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
match &self.workspace_switch {
Some(switch) => {
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 offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
(before_idx, Point::from((0, offset)))
} else {
(after_idx, Point::from((0, -size.h + offset)))
};
let ws = &self.workspaces[idx];
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
Some((win, win_pos.map(|p| p - ws_offset)))
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.window_under(pos_within_output)
}
}
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() {
return false;
}
let ws = &self.workspaces[self.active_workspace_idx];
ws.render_above_top_layer()
}
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
let output_transform = self.output.current_transform();
let output_mode = self.output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
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 offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let before = self.workspaces[before_idx].render_elements(renderer);
let after = self.workspaces[after_idx].render_elements(renderer);
// HACK: crop to infinite bounds for all sides except the side where the workspaces
// join, to decrease the chance of cutting a lower-scale surface in the middle of a
// pixel, thereby disabling its nearest-neighbor upscaling.
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
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(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
before.chain(after).collect()
}
None => {
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
elements
.into_iter()
.filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: set infinite crop bounds due to a damage tracking bug
// which causes glitched rendering for maximized GTK windows.
// FIXME: use proper bounds after fixing the Crop element.
Rectangle::from_loc_and_size(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX, i32::MAX),
),
// Rectangle::from_loc_and_size((0, 0), size),
)?,
(0, 0),
Relocate::Relative,
))
})
.collect()
}
}
}
}
+393
View File
@@ -0,0 +1,393 @@
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::FocusRing;
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)]
pub struct Tile<W: LayoutElement> {
/// The toplevel window itself.
window: W,
/// 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,
/// to avoid black backdrop flicker before the window has had a chance to resize.
is_fullscreen: bool,
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
/// 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 => {
LayoutElement = LayoutElementRenderElement<R>,
SolidColor = RelocateRenderElement<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.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.into());
self.focus_ring.update_config(options.focus_ring);
self.options = options;
}
pub fn update_window(&mut self) {
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0, 0)) {
self.is_fullscreen = self.window.is_fullscreen();
}
}
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(),
);
self.border.set_active(is_active);
self.focus_ring
.update((0, 0).into(), 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.,
self.options.animations.window_open,
niri_config::Animation::default_window_open(),
));
}
pub fn window(&self) -> &W {
&self.window
}
pub fn into_window(self) -> W {
self.window
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<i32> {
if self.is_fullscreen {
return None;
}
if self.border.is_off() {
return None;
}
Some(self.border.width())
}
/// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window.size();
let target_size = self.fullscreen_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
if window_size.w < target_size.w {
loc.x += (target_size.w - window_size.w) / 2;
}
if window_size.h < target_size.h {
loc.y += (target_size.h - window_size.h) / 2;
}
}
if let Some(width) = self.effective_border_width() {
loc += (width, width).into();
}
loc
}
pub fn tile_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = max(size.w, self.fullscreen_size.w);
size.h = max(size.h, self.fullscreen_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
}
size
}
pub fn window_size(&self) -> Size<i32, Logical> {
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.);
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();
loc += self.window.buf_loc();
loc
}
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
point -= self.window_loc().to_f64();
self.window.is_in_input_region(point)
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0, 0), self.tile_size());
activation_region.to_f64().contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
size.w = max(1, size.w - width * 2);
size.h = max(1, size.h - width * 2);
}
self.window.request_size(size);
}
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
}
}
pub fn tile_height_for_window_height(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
}
}
pub fn window_height_for_tile_height(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_sub(self.border.width() * 2)
}
}
pub fn request_fullscreen(&mut self, size: Size<i32, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size);
}
pub fn min_size(&self) -> Size<i32, Logical> {
let mut size = self.window.min_size();
if let Some(width) = self.effective_border_width() {
size.w = max(1, size.w);
size.h = max(1, size.h);
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
}
size
}
pub fn max_size(&self) -> Size<i32, Logical> {
let mut size = self.window.max_size();
if let Some(width) = self.effective_border_width() {
if size.w > 0 {
size.w = size.w.saturating_add(width * 2);
}
if size.h > 0 {
size.h = size.h.saturating_add(width * 2);
}
}
size
}
pub fn has_ssd(&self) -> bool {
self.effective_border_width().is_some() || self.window.has_ssd()
}
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
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 elem = self.effective_border_width().map(|_| {
self.border.render(scale).map(move |elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
.into()
})
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| {
self.focus_ring.render(scale).map(move |elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
.into()
})
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = self.is_fullscreen.then(|| {
let elem = SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
);
RelocateRenderElement::from_element(elem, (0, 0), Relocate::Relative).into()
});
rv.chain(elem)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
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, focus_ring);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
renderer,
scale.x as i32,
&elements,
anim.value() 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).min(1.),
),
))
.into_iter()
.chain(None.into_iter().flatten())
} else {
self.window().set_offscreen_element_id(None);
let elements = self.render_inner(renderer, location, scale, focus_ring);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
}
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
#[macro_use]
extern crate tracing;
pub mod animation;
pub mod backend;
pub mod cli;
pub mod config_error_notification;
pub mod cursor;
#[cfg(feature = "dbus")]
pub mod dbus;
pub mod exit_confirm_dialog;
pub mod frame_clock;
pub mod handlers;
pub mod hotkey_overlay;
pub mod input;
pub mod ipc;
pub mod layout;
pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod screenshot_ui;
pub mod utils;
pub mod watcher;
#[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;
+121 -79
View File
@@ -1,74 +1,30 @@
#[macro_use]
extern crate tracing;
mod animation;
mod backend;
mod config;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod frame_clock;
mod handlers;
mod input;
mod layout;
mod niri;
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::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
use config::Config;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
use miette::{Context, NarratableReportHandler};
use niri::{Niri, State};
use clap::Parser;
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::{
cause_panic, spawn, version, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::watcher::Watcher;
use niri_config::Config;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::{self, EventLoop};
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
use utils::spawn;
use watcher::Watcher;
use crate::utils::{REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
#[derive(Parser)]
#[command(author, 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>,
},
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
@@ -109,34 +65,89 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let _client = tracy_client::Client::start();
// Set a better error printer for config loading.
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
niri_config::set_miette_hook().unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
Config::load(config).context("error loading config")?;
let path = config
.or_else(default_config_path)
.expect("error getting config path");
Config::load(&path)?;
info!("config is valid");
return Ok(());
}
Sub::Msg { msg, json } => {
handle_msg(msg, json)?;
return Ok(());
}
Sub::Panic => cause_panic(),
}
}
info!(
"starting version {} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
);
info!("starting version {}", &version());
// Load the config.
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
Ok((config, path)) => (config, Some(path)),
Err(err) => {
warn!("{err:?}");
(Config::default(), None)
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
.as_deref()
.and_then(|path| match Config::load(path) {
Ok(config) => Some(config),
Err(err) => {
warn!("{err:?}");
config_errored = true;
None
}
})
.unwrap_or_default();
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
// Create the compositor.
@@ -147,7 +158,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;
@@ -157,14 +169,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
socket_name.to_string_lossy()
);
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
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();
// Inhibit power key handling so we can suspend on it.
#[cfg(feature = "dbus")]
if let Err(err) = state.niri.inhibit_power_key() {
warn!("error inhibiting power key: {err:?}");
if !state.niri.config.borrow().input.disable_power_key_handling {
if let Err(err) = state.niri.inhibit_power_key() {
warn!("error inhibiting power key: {err:?}");
}
}
}
@@ -177,7 +197,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// 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
@@ -199,6 +219,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
spawn(elem.command);
}
// 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.
event_loop
.run(None, &mut state, |state| state.refresh_and_flush_clients())
@@ -208,12 +235,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
fn import_env_to_systemd() {
let variables = ["WAYLAND_DISPLAY", niri_ipc::SOCKET_PATH_ENV].join(" ");
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!(
"systemctl --user import-environment {variables} && \
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
@@ -234,3 +265,14 @@ fn import_env_to_systemd() {
}
}
}
fn default_config_path() -> Option<PathBuf> {
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
warn!("error retrieving home directory");
return None;
};
let mut path = dirs.config_dir().to_owned();
path.push("config.kdl");
Some(path)
}
+864 -403
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().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().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);
};
}
+1
View File
@@ -0,0 +1 @@
pub mod foreign_toplevel;
+35 -19
View File
@@ -7,18 +7,22 @@ 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;
@@ -27,22 +31,24 @@ use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, 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();
@@ -95,21 +101,31 @@ impl PipeWire {
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
let to_niri_ = to_niri.clone();
let stop_cast = move || {
if let Err(err) = to_niri.send(ScreenCastToNiri::StopCast { session_id }) {
if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) {
warn!("error sending StopCast to niri: {err:?}");
}
};
let weak = output.downgrade();
let redraw = move || {
if let Some(output) = weak.upgrade() {
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) {
warn!("error sending Redraw to niri: {err:?}");
}
}
};
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));
@@ -118,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 {
@@ -158,13 +173,14 @@ impl PipeWire {
StreamState::Connecting => (),
StreamState::Streaming => {
is_active.set(true);
redraw();
}
}
}
})
.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");
@@ -246,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();
}
@@ -255,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 {
@@ -299,7 +314,7 @@ impl PipeWire {
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: remove_buffer");
unsafe {
@@ -373,6 +388,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+131
View File
@@ -0,0 +1,131 @@
use anyhow::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::{Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
pub mod nearest_integer_scale;
pub mod offscreen;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
pub fn render_to_texture(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
let _span = tracy_client::span!();
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
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 mut frame = renderer
.render(size, Transform::Normal)
.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")?;
}
}
let sync_point = frame.finish().context("error finishing frame")?;
Ok((texture, sync_point))
}
pub fn render_and_download(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
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, 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>,
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, 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>,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
renderer.bind(dmabuf).context("error binding texture")?;
let mut frame = renderer
.render(size, Transform::Normal)
.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")?;
}
}
let _sync_point = frame.finish().context("error finishing frame")?;
Ok(())
}
+100
View File
@@ -0,0 +1,100 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{Frame, Renderer, TextureFilter};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
#[derive(Debug)]
pub struct NearestIntegerScale<E: Element>(E);
impl<E: Element> From<E> for NearestIntegerScale<E> {
fn from(value: E) -> Self {
Self(value)
}
}
impl<E: Element> Element for NearestIntegerScale<E> {
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<R: Renderer, E: RenderElement<R>> RenderElement<R> for NearestIntegerScale<E> {
fn draw(
&self,
frame: &mut <R as Renderer>::Frame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), R::Error> {
let mut use_nearest = false;
// Check that we don't need to interpolate between src pixels.
let src_i32 = src.to_i32_down::<i32>();
if src_i32.to_f64() == src {
// Check that the src is not zero.
if !src_i32.size.is_empty() {
// Check that the scale factor is an integer.
let scale_x = dst.size.w / src_i32.size.w;
let scale_y = dst.size.h / src_i32.size.h;
if scale_x * src_i32.size.w == dst.size.w && scale_y * src_i32.size.h == dst.size.h
{
use_nearest = true;
}
}
}
let mut prev_filter = TextureFilter::Linear;
if use_nearest {
prev_filter = frame.upscale_filter();
frame.set_upscale_filter(TextureFilter::Nearest);
}
let rv = self.0.draw(frame, src, dst, damage);
if use_nearest {
frame.set_upscale_filter(prev_filter);
}
rv
}
fn underlying_storage(&self, renderer: &mut R) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+218
View File
@@ -0,0 +1,218 @@
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),
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, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
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, 'alloc>,
) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
+99
View File
@@ -0,0 +1,99 @@
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::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
impl Element for PrimaryGpuTextureRenderElement {
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 PrimaryGpuTextureRenderElement {
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, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
for PrimaryGpuTextureRenderElement
{
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
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, 'alloc>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+126
View File
@@ -0,0 +1,126 @@
// We need to implement RenderElement manually due to AsGlesFrame requirement.
// This macro does it for us.
#[macro_export]
macro_rules! niri_render_elements {
($name: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>) -> 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<smithay::backend::renderer::gles::GlesRenderer> {
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, 'alloc> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render, 'alloc>>
for $name<$crate::backend::tty::TtyRenderer<'render, 'alloc>>
{
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, 'alloc, '_>,
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, 'alloc>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render, 'alloc>>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(
&self,
renderer: &mut $crate::backend::tty::TtyRenderer<'render, 'alloc>,
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
}
+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, '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()
}
}
+161 -21
View File
@@ -5,19 +5,21 @@ use std::mem;
use anyhow::Context;
use arrayvec::ArrayVec;
use niri_config::Action;
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::Kind;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
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::ExportMem;
use smithay::input::keyboard::{Keysym, ModifiersState};
use smithay::output::{Output, WeakOutput};
use smithay::render_elements;
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use crate::config::Action;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
const BORDER: i32 = 2;
@@ -39,17 +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],
}
render_elements! {
#[derive(Debug)]
pub ScreenshotUiRenderElement<R>;
Screenshot = TextureRenderElement<R::TextureId>,
SolidColor = SolidColorRenderElement,
#[derive(Debug)]
pub enum ScreenshotUiRenderElement {
Screenshot(PrimaryGpuTextureRenderElement),
SolidColor(SolidColorRenderElement),
}
impl ScreenshotUi {
@@ -92,6 +95,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -102,13 +106,14 @@ 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,
texture.clone(),
output.current_scale().integer_scale(),
scale,
Transform::Normal,
None,
);
@@ -125,6 +130,8 @@ impl ScreenshotUi {
let locations = [Default::default(); 8];
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -236,10 +243,7 @@ impl ScreenshotUi {
}
}
pub fn render_output(
&self,
output: &Output,
) -> ArrayVec<ScreenshotUiRenderElement<GlesRenderer>, 9> {
pub fn render_output(&self, output: &Output) -> ArrayVec<ScreenshotUiRenderElement, 9> {
let _span = tracy_client::span!("ScreenshotUi::render_output");
let Self::Open { output_data, .. } = self else {
@@ -266,14 +270,14 @@ impl ScreenshotUi {
// The screenshot itself goes last.
elements.push(
TextureRenderElement::from_texture_buffer(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
(0., 0.),
&output_data.texture_buffer,
None,
None,
None,
Kind::Unspecified,
)
))
.into(),
);
@@ -332,9 +336,10 @@ impl ScreenshotUi {
}
}
pub fn output_size(&self, output: &Output) -> Option<Size<i32, Physical>> {
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
if let Self::Open { output_data, .. } = self {
Some(output_data.get(output)?.size)
let data = output_data.get(output)?;
Some((data.size, data.scale, data.transform))
} else {
None
}
@@ -446,3 +451,138 @@ 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)
}
}
+32 -2
View File
@@ -11,10 +11,23 @@ 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};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::config::Config;
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);
@@ -25,6 +38,16 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
rect.loc + rect.size.downscale(2).to_point()
}
pub fn output_size(output: &Output) -> Size<i32, Logical> {
let output_scale = output.current_scale().integer_scale();
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
output_transform
.transform_size(output_mode.size)
.to_logical(output_scale)
}
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
let Some(path) = &config.screenshot_path else {
return Ok(None);
@@ -176,3 +199,10 @@ pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
warn!("error showing screenshot notification: {err:?}");
}
}
#[inline(never)]
pub fn cause_panic() {
let a = Duration::from_secs(1);
let b = Duration::from_secs(2);
let _ = a - b;
}
+18 -4
View File
@@ -27,7 +27,18 @@ impl Watcher {
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();
// 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();
loop {
thread::sleep(Duration::from_millis(500));
@@ -36,8 +47,11 @@ impl Watcher {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
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(()) {
@@ -45,7 +59,7 @@ impl Watcher {
break;
}
last_mtime = Some(mtime);
last_props = Some(new_props);
}
}
}