Compare commits

..

188 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
65 changed files with 9615 additions and 1810 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
+818 -265
View File
File diff suppressed because it is too large Load Diff
+36 -21
View File
@@ -1,5 +1,8 @@
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.0-alpha.3"
version = "0.1.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -7,17 +10,23 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
bitflags = "2.4.1"
directories = "5.0.1"
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/Smithay/smithay.git"
git = "https://github.com/YaLTeR/smithay.git"
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
git = "https://github.com/YaLTeR/smithay.git"
rev = "0c06b7889b72e4392d89fab91f2d2cf4f272db83"
# path = "../smithay/smithay-drm-extras"
[package]
@@ -33,33 +42,38 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.79" }
anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.1.1", optional = true }
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.1"
clap = { version = "4.4.13", 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 }
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"] }
niri-config = { version = "0.1.0-alpha.3", path = "niri-config" }
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.13"
profiling = "1.0.14"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.113"
smithay-drm-extras.workspace = true
serde = { version = "1.0.195", features = ["derive"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client = { version = "0.16.5", default-features = false }
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]
workspace = true
@@ -73,6 +87,7 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
@@ -85,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.
@@ -101,7 +116,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~alpha.3"
version = "0.1.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+84 -35
View File
@@ -1,35 +1,55 @@
# 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
@@ -37,25 +57,36 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
> 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
@@ -94,10 +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>.
@@ -107,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) |
@@ -122,13 +167,13 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <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 |
@@ -153,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"
];
};
}
);
}
+3 -2
View File
@@ -9,8 +9,9 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
directories.workspace = true
knuffel = "3.2.0"
miette = "5.10.0"
smithay.workspace = true
niri-ipc = { version = "0.1.1", path = "../niri-ipc" }
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+434 -114
View File
@@ -1,15 +1,16 @@
#[macro_use]
extern crate tracing;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{LayoutSwitchTarget, SizeChange};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig};
use smithay::reexports::input;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
@@ -34,6 +35,10 @@ pub struct Config {
]
pub screenshot_path: Option<String>,
#[knuffel(child, default)]
pub hotkey_overlay: HotkeyOverlay,
#[knuffel(child, default)]
pub animations: Animations,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
pub debug: DebugConfig,
@@ -47,6 +52,8 @@ pub struct Input {
#[knuffel(child, default)]
pub touchpad: Touchpad,
#[knuffel(child, default)]
pub mouse: Mouse,
#[knuffel(child, default)]
pub tablet: Tablet,
#[knuffel(child)]
pub disable_power_key_handling: bool,
@@ -91,6 +98,18 @@ impl Xkb {
}
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum CenterFocusedColumn {
/// Focusing a column will not center the column.
#[default]
Never,
/// The focused column will always be centered.
Always,
/// Focusing a column will center it if it doesn't fit on the screen together with the
/// previously focused column.
OnOverflow,
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
pub enum TrackLayout {
/// The layout change is global.
@@ -106,9 +125,57 @@ pub struct Touchpad {
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub dwt: bool,
#[knuffel(child)]
pub dwtp: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub tap_button_map: Option<TapButtonMap>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Mouse {
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccelProfile {
Adaptive,
Flat,
}
impl From<AccelProfile> for input::AccelProfile {
fn from(value: AccelProfile) -> Self {
match value {
AccelProfile::Adaptive => Self::Adaptive,
AccelProfile::Flat => Self::Flat,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TapButtonMap {
LeftRightMiddle,
LeftMiddleRight,
}
impl From<TapButtonMap> for input::TapButtonMap {
fn from(value: TapButtonMap) -> Self {
match value {
TapButtonMap::LeftRightMiddle => Self::LeftRightMiddle,
TapButtonMap::LeftMiddleRight => Self::LeftMiddleRight,
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -125,6 +192,8 @@ pub struct Output {
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
#[knuffel(child, unwrap(argument, str), default = Transform::Normal)]
pub transform: Transform,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
@@ -137,13 +206,63 @@ impl Default for Output {
off: false,
name: String::new(),
scale: 1.,
transform: Transform::Normal,
position: None,
mode: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
/// Output transform, which goes counter-clockwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
Normal,
_90,
_180,
_270,
Flipped,
Flipped90,
Flipped180,
Flipped270,
}
impl FromStr for Transform {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"90" => Ok(Self::_90),
"180" => Ok(Self::_180),
"270" => Ok(Self::_270),
"flipped" => Ok(Self::Flipped),
"flipped-90" => Ok(Self::Flipped90),
"flipped-180" => Ok(Self::Flipped180),
"flipped-270" => Ok(Self::Flipped270),
_ => Err(miette!(concat!(
r#"invalid transform, can be "90", "180", "270", "#,
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
))),
}
}
}
impl From<Transform> for smithay::utils::Transform {
fn from(value: Transform) -> Self {
match value {
Transform::Normal => Self::Normal,
Transform::_90 => Self::_90,
Transform::_180 => Self::_180,
Transform::_270 => Self::_270,
Transform::Flipped => Self::Flipped,
Transform::Flipped90 => Self::Flipped90,
Transform::Flipped180 => Self::Flipped180,
Transform::Flipped270 => Self::Flipped270,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
@@ -151,7 +270,7 @@ pub struct Position {
pub y: i32,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
@@ -162,12 +281,14 @@ pub struct Mode {
pub struct Layout {
#[knuffel(child, default)]
pub focus_ring: FocusRing,
#[knuffel(child, default = default_border())]
pub border: FocusRing,
#[knuffel(child, default)]
pub border: Border,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetWidth>,
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument), default)]
pub center_focused_column: CenterFocusedColumn,
#[knuffel(child, unwrap(argument), default = 16)]
pub gaps: u16,
#[knuffel(child, default)]
@@ -184,11 +305,11 @@ pub struct SpawnAtStartup {
pub struct FocusRing {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = 4)]
#[knuffel(child, unwrap(argument), default = Self::default().width)]
pub width: u16,
#[knuffel(child, default = Color::new(127, 200, 255, 255))]
#[knuffel(child, default = Self::default().active_color)]
pub active_color: Color,
#[knuffel(child, default = Color::new(80, 80, 80, 255))]
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
}
@@ -203,12 +324,37 @@ impl Default for FocusRing {
}
}
pub const fn default_border() -> FocusRing {
FocusRing {
off: true,
width: 4,
active_color: Color::new(255, 200, 127, 255),
inactive_color: Color::new(80, 80, 80, 255),
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Border {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = Self::default().width)]
pub width: u16,
#[knuffel(child, default = Self::default().active_color)]
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
}
impl Default for Border {
fn default() -> Self {
Self {
off: true,
width: 4,
active_color: Color::new(255, 200, 127, 255),
inactive_color: Color::new(80, 80, 80, 255),
}
}
}
impl From<Border> for FocusRing {
fn from(value: Border) -> Self {
Self {
off: value.off,
width: value.width,
active_color: value.active_color,
inactive_color: value.inactive_color,
}
}
}
@@ -232,7 +378,8 @@ impl Color {
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.)
let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.);
[r * a, g * a, b * a, a]
}
}
@@ -274,6 +421,95 @@ pub struct Struts {
pub bottom: u16,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HotkeyOverlay {
#[knuffel(child)]
pub skip_at_startup: bool,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Animations {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = 1.)]
pub slowdown: f64,
#[knuffel(child, default = Animation::default_workspace_switch())]
pub workspace_switch: Animation,
#[knuffel(child, default = Animation::default_horizontal_view_movement())]
pub horizontal_view_movement: Animation,
#[knuffel(child, default = Animation::default_window_open())]
pub window_open: Animation,
#[knuffel(child, default = Animation::default_config_notification_open_close())]
pub config_notification_open_close: Animation,
}
impl Default for Animations {
fn default() -> Self {
Self {
off: false,
slowdown: 1.,
workspace_switch: Animation::default_workspace_switch(),
horizontal_view_movement: Animation::default_horizontal_view_movement(),
window_open: Animation::default_window_open(),
config_notification_open_close: Animation::default_config_notification_open_close(),
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Animation {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument))]
pub duration_ms: Option<u32>,
#[knuffel(child, unwrap(argument))]
pub curve: Option<AnimationCurve>,
}
impl Animation {
pub const fn unfilled() -> Self {
Self {
off: false,
duration_ms: None,
curve: None,
}
}
pub const fn default() -> Self {
Self {
off: false,
duration_ms: Some(250),
curve: Some(AnimationCurve::EaseOutCubic),
}
}
pub const fn default_workspace_switch() -> Self {
Self::default()
}
pub const fn default_horizontal_view_movement() -> Self {
Self::default()
}
pub const fn default_config_notification_open_close() -> Self {
Self::default()
}
pub const fn default_window_open() -> Self {
Self {
duration_ms: Some(150),
curve: Some(AnimationCurve::EaseOutExpo),
..Self::default()
}
}
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
pub enum AnimationCurve {
EaseOutCubic,
EaseOutExpo,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
@@ -302,9 +538,10 @@ bitflags! {
}
}
// Remember to add new actions to the CLI enum too.
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum Action {
Quit,
Quit(#[knuffel(property(name = "skip-confirmation"), default)] bool),
#[knuffel(skip)]
ChangeVt(i32),
Suspend,
@@ -336,6 +573,8 @@ pub enum Action {
MoveWindowUp,
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeOrExpelWindowLeft,
ConsumeOrExpelWindowRight,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
@@ -345,6 +584,9 @@ pub enum Action {
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[knuffel(argument)] u8),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
@@ -355,31 +597,96 @@ pub enum Action {
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
MoveColumnToMonitorLeft,
MoveColumnToMonitorRight,
MoveColumnToMonitorDown,
MoveColumnToMonitorUp,
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget),
ShowHotkeyOverlay,
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorUp,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
SetFixed(i32),
SetProportion(f64),
AdjustFixed(i32),
AdjustProportion(f64),
impl From<niri_ipc::Action> for Action {
fn from(value: niri_ipc::Action) -> Self {
match value {
niri_ipc::Action::Quit { skip_confirmation } => Self::Quit(skip_confirmation),
niri_ipc::Action::PowerOffMonitors => Self::PowerOffMonitors,
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
niri_ipc::Action::Screenshot => Self::Screenshot,
niri_ipc::Action::ScreenshotScreen => Self::ScreenshotScreen,
niri_ipc::Action::ScreenshotWindow => Self::ScreenshotWindow,
niri_ipc::Action::CloseWindow => Self::CloseWindow,
niri_ipc::Action::FullscreenWindow => Self::FullscreenWindow,
niri_ipc::Action::FocusColumnLeft => Self::FocusColumnLeft,
niri_ipc::Action::FocusColumnRight => Self::FocusColumnRight,
niri_ipc::Action::FocusColumnFirst => Self::FocusColumnFirst,
niri_ipc::Action::FocusColumnLast => Self::FocusColumnLast,
niri_ipc::Action::FocusWindowDown => Self::FocusWindowDown,
niri_ipc::Action::FocusWindowUp => Self::FocusWindowUp,
niri_ipc::Action::FocusWindowOrWorkspaceDown => Self::FocusWindowOrWorkspaceDown,
niri_ipc::Action::FocusWindowOrWorkspaceUp => Self::FocusWindowOrWorkspaceUp,
niri_ipc::Action::MoveColumnLeft => Self::MoveColumnLeft,
niri_ipc::Action::MoveColumnRight => Self::MoveColumnRight,
niri_ipc::Action::MoveColumnToFirst => Self::MoveColumnToFirst,
niri_ipc::Action::MoveColumnToLast => Self::MoveColumnToLast,
niri_ipc::Action::MoveWindowDown => Self::MoveWindowDown,
niri_ipc::Action::MoveWindowUp => Self::MoveWindowUp,
niri_ipc::Action::MoveWindowDownOrToWorkspaceDown => {
Self::MoveWindowDownOrToWorkspaceDown
}
niri_ipc::Action::MoveWindowUpOrToWorkspaceUp => Self::MoveWindowUpOrToWorkspaceUp,
niri_ipc::Action::ConsumeOrExpelWindowLeft => Self::ConsumeOrExpelWindowLeft,
niri_ipc::Action::ConsumeOrExpelWindowRight => Self::ConsumeOrExpelWindowRight,
niri_ipc::Action::ConsumeWindowIntoColumn => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn => Self::ExpelWindowFromColumn,
niri_ipc::Action::CenterColumn => Self::CenterColumn,
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown,
niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp,
niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index),
niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp,
niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft,
niri_ipc::Action::FocusMonitorRight => Self::FocusMonitorRight,
niri_ipc::Action::FocusMonitorDown => Self::FocusMonitorDown,
niri_ipc::Action::FocusMonitorUp => Self::FocusMonitorUp,
niri_ipc::Action::MoveWindowToMonitorLeft => Self::MoveWindowToMonitorLeft,
niri_ipc::Action::MoveWindowToMonitorRight => Self::MoveWindowToMonitorRight,
niri_ipc::Action::MoveWindowToMonitorDown => Self::MoveWindowToMonitorDown,
niri_ipc::Action::MoveWindowToMonitorUp => Self::MoveWindowToMonitorUp,
niri_ipc::Action::MoveColumnToMonitorLeft => Self::MoveColumnToMonitorLeft,
niri_ipc::Action::MoveColumnToMonitorRight => Self::MoveColumnToMonitorRight,
niri_ipc::Action::MoveColumnToMonitorDown => Self::MoveColumnToMonitorDown,
niri_ipc::Action::MoveColumnToMonitorUp => Self::MoveColumnToMonitorUp,
niri_ipc::Action::SetWindowHeight { change } => Self::SetWindowHeight(change),
niri_ipc::Action::SwitchPresetColumnWidth => Self::SwitchPresetColumnWidth,
niri_ipc::Action::MaximizeColumn => Self::MaximizeColumn,
niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change),
niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout),
niri_ipc::Action::ShowHotkeyOverlay => Self::ShowHotkeyOverlay,
niri_ipc::Action::MoveWorkspaceToMonitorLeft => Self::MoveWorkspaceToMonitorLeft,
niri_ipc::Action::MoveWorkspaceToMonitorRight => Self::MoveWorkspaceToMonitorRight,
niri_ipc::Action::MoveWorkspaceToMonitorDown => Self::MoveWorkspaceToMonitorDown,
niri_ipc::Action::MoveWorkspaceToMonitorUp => Self::MoveWorkspaceToMonitorUp,
niri_ipc::Action::ToggleDebugTint => Self::ToggleDebugTint,
}
}
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
pub enum LayoutAction {
Next,
Prev,
}
#[derive(knuffel::Decode, Debug, PartialEq)]
#[derive(knuffel::Decode, Debug, Default, 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)]
@@ -394,47 +701,24 @@ pub struct DebugConfig {
pub render_drm_device: Option<PathBuf>,
}
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,
disable_cursor_plane: false,
render_drm_device: None,
}
}
}
impl Config {
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
pub fn load(path: &Path) -> miette::Result<Self> {
let _span = tracy_client::span!("Config::load");
Self::load_internal(path).context("error loading config")
}
fn load_internal(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)
fn load_internal(path: &Path) -> miette::Result<Self> {
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))
Ok(config)
}
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
let _span = tracy_client::span!("Config::parse");
knuffel::parse(filename, text)
}
}
@@ -519,54 +803,30 @@ impl FromStr for Key {
}
}
impl FromStr for SizeChange {
impl FromStr for AccelProfile {
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 s {
"adaptive" => Ok(Self::Adaptive),
"flat" => Ok(Self::Flat),
_ => Err(miette!(
r#"invalid accel profile, can be "adaptive" or "flat""#
)),
}
}
}
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")),
}
}
impl FromStr for TapButtonMap {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left-right-middle" => Ok(Self::LeftRightMiddle),
"left-middle-right" => Ok(Self::LeftMiddleRight),
_ => Err(miette!(
r#"invalid tap button map, can be "left-right-middle" or "left-middle-right""#
)),
}
}
}
@@ -608,7 +868,17 @@ mod tests {
touchpad {
tap
dwt
dwtp
accel-speed 0.2
accel-profile "flat"
tap-button-map "left-middle-right"
}
mouse {
natural-scroll
accel-speed 0.4
accel-profile "flat"
}
tablet {
@@ -620,6 +890,7 @@ mod tests {
output "eDP-1" {
scale 2.0
transform "flipped-90"
position x=10 y=20
mode "1920x1080@144"
}
@@ -633,7 +904,6 @@ mod tests {
border {
width 3
active-color 0 100 200 255
inactive-color 255 200 100 0
}
@@ -653,6 +923,8 @@ mod tests {
right 2
top 3
}
center-focused-column "on-overflow"
}
spawn-at-startup "alacritty" "-e" "fish"
@@ -666,17 +938,32 @@ mod tests {
screenshot-path "~/Screenshots/screenshot.png"
hotkey-overlay {
skip-at-startup
}
animations {
slowdown 2.0
workspace-switch { off; }
horizontal-view-movement {
duration-ms 100
curve "ease-out-expo"
}
}
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;}
Mod+1 { focus-workspace 1; }
Mod+Shift+E { quit skip-confirmation=true; }
}
debug {
animation-slowdown 2.0
render-drm-device "/dev/dri/renderD129"
}
"#,
@@ -694,8 +981,17 @@ mod tests {
},
touchpad: Touchpad {
tap: true,
dwt: true,
dwtp: true,
natural_scroll: false,
accel_speed: 0.2,
accel_profile: Some(AccelProfile::Flat),
tap_button_map: Some(TapButtonMap::LeftMiddleRight),
},
mouse: Mouse {
natural_scroll: true,
accel_speed: 0.4,
accel_profile: Some(AccelProfile::Flat),
},
tablet: Tablet {
map_to_output: Some("eDP-1".to_owned()),
@@ -706,6 +1002,7 @@ mod tests {
off: false,
name: "eDP-1".to_owned(),
scale: 2.,
transform: Transform::Flipped90,
position: Some(Position { x: 10, y: 20 }),
mode: Some(Mode {
width: 1920,
@@ -730,13 +1027,13 @@ mod tests {
a: 0,
},
},
border: FocusRing {
border: Border {
off: false,
width: 3,
active_color: Color {
r: 0,
g: 100,
b: 200,
r: 255,
g: 200,
b: 127,
a: 255,
},
inactive_color: Color {
@@ -762,6 +1059,7 @@ mod tests {
top: 3,
bottom: 0,
},
center_focused_column: CenterFocusedColumn::OnOverflow,
},
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
@@ -772,6 +1070,22 @@ mod tests {
xcursor_size: 16,
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
hotkey_overlay: HotkeyOverlay {
skip_at_startup: true,
},
animations: Animations {
slowdown: 2.,
workspace_switch: Animation {
off: true,
..Animation::unfilled()
},
horizontal_view_movement: Animation {
duration_ms: Some(100),
curve: Some(AnimationCurve::EaseOutExpo),
..Animation::unfilled()
},
..Default::default()
},
binds: Binds(vec![
Bind {
key: Key {
@@ -815,9 +1129,15 @@ mod tests {
},
actions: vec![Action::FocusWorkspace(1)],
},
Bind {
key: Key {
keysym: Keysym::e,
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
actions: vec![Action::Quit(true)],
},
]),
debug: DebugConfig {
animation_slowdown: 2.,
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
..Default::default()
},
+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
}
}
+186 -42
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 {
@@ -45,7 +55,8 @@ input {
// 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" {
@@ -55,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.
@@ -76,6 +91,14 @@ input {
}
layout {
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side 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.
@@ -107,9 +130,9 @@ layout {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.33333
proportion 0.5
proportion 0.667
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
@@ -134,6 +157,14 @@ layout {
// 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.
@@ -161,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
@@ -168,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+"; }
@@ -180,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.
@@ -210,37 +307,49 @@ binds {
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
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+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; }
@@ -251,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; }
@@ -296,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,
}
}
}
+23 -7
View File
@@ -1,4 +1,6 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -8,7 +10,7 @@ use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::Niri;
use crate::niri::Niri;
pub mod tty;
pub use tty::Tty;
@@ -96,7 +98,7 @@ impl Backend {
}
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
@@ -110,11 +112,18 @@ impl Backend {
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
match self {
Backend::Tty(tty) => tty.connectors(),
Backend::Winit(winit) => winit.connectors(),
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(),
}
}
@@ -129,13 +138,20 @@ impl Backend {
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
}
}
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v
+481 -184
View File
@@ -20,7 +20,7 @@ use smithay::backend::drm::{
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -28,11 +28,11 @@ use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
use smithay::desktop::utils::OutputPresentationFeedback;
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel};
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
use smithay::reexports::drm::control::{
connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags,
self, connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags,
};
use smithay::reexports::gbm::Modifier;
use smithay::reexports::input::Libinput;
@@ -41,16 +41,19 @@ use smithay::reexports::wayland_protocols;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::DeviceFd;
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use super::RenderResult;
use crate::niri::{RedrawState, State};
use crate::render_helpers::AsGlesRenderer;
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::utils::get_monotonic_time;
use crate::Niri;
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -73,7 +76,10 @@ pub struct Tty {
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
// the primary GPU.
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
connectors: Arc<Mutex<HashMap<String, Output>>>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
@@ -102,7 +108,7 @@ type GbmDrmCompositor = DrmCompositor<
DrmDeviceFd,
>;
struct OutputDevice {
pub struct OutputDevice {
token: RegistrationToken,
render_node: DrmNode,
drm_scanner: DrmScanner,
@@ -111,6 +117,42 @@ struct OutputDevice {
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
gbm: GbmDevice<DrmDeviceFd>,
pub drm_lease_state: DrmLeaseState,
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
active_leases: Vec<DrmLease>,
}
impl OutputDevice {
pub fn lease_request(
&self,
request: DrmLeaseRequest,
) -> Result<DrmLeaseBuilder, LeaseRejected> {
let mut builder = DrmLeaseBuilder::new(&self.drm);
for connector in request.connectors {
let (_, crtc) = self
.non_desktop_connectors
.iter()
.find(|(conn, _)| connector == *conn)
.ok_or_else(|| {
warn!("Attempted to lease connector that is not non-desktop");
LeaseRejected::default()
})?;
builder.add_connector(connector);
builder.add_crtc(*crtc);
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
builder.add_plane(planes.primary.handle);
}
Ok(builder)
}
pub fn new_lease(&mut self, lease: DrmLease) {
self.active_leases.push(lease);
}
pub fn remove_lease(&mut self, lease_id: u32) {
self.active_leases.retain(|l| l.id() != lease_id);
}
}
#[derive(Debug, Clone, Copy)]
@@ -140,11 +182,19 @@ pub struct SurfaceDmabufFeedback {
}
impl Tty {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
let (session, notifier) = LibSeatSession::new().unwrap();
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<'static, State>,
) -> anyhow::Result<Self> {
let (session, notifier) = LibSeatSession::new().context(
"Error creating a session. This might mean that you're trying to run niri on a TTY \
that is already busy, for example if you're running this inside tmux that had been \
originally started on a different TTY",
)?;
let seat_name = session.seat();
let udev_backend = UdevBackend::new(session.seat()).unwrap();
let udev_backend =
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
state.backend.tty().on_udev_event(&mut state.niri, event);
});
@@ -153,7 +203,9 @@ impl Tty {
.unwrap();
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput.udev_assign_seat(&seat_name).unwrap();
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
@@ -188,18 +240,23 @@ impl Tty {
Ok(gles)
};
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
let gpu_manager = GpuManager::new(api).unwrap();
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
.unwrap_or_else(|| {
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
.ok_or(())
.or_else(|()| {
let primary_gpu_path = udev::primary_gpu(&seat_name)
.context("error getting the primary GPU")?
.context("couldn't find a GPU")?;
let primary_node = DrmNode::from_path(primary_gpu_path)
.context("error opening the primary GPU DRM node")?;
let primary_render_node = primary_node
.node_with_type(NodeType::Render)
.unwrap()
.unwrap();
(primary_node, primary_render_node)
});
.context("error getting the render node for the primary GPU")?
.context("error getting the render node for the primary GPU")?;
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
})?;
let mut node_path = String::new();
if let Some(path) = primary_render_node.dev_path() {
@@ -209,7 +266,7 @@ impl Tty {
}
info!("using as the render node: {}", node_path);
Self {
Ok(Self {
config,
session,
udev_dispatcher,
@@ -220,8 +277,10 @@ impl Tty {
devices: HashMap::new(),
dmabuf_global: None,
primary_allocator: None,
connectors: Arc::new(Mutex::new(HashMap::new())),
}
update_output_config_on_resume: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn init(&mut self, niri: &mut Niri) {
@@ -274,7 +333,7 @@ impl Tty {
self.libinput.suspend();
for device in self.devices.values() {
for device in self.devices.values_mut() {
device.drm.pause();
}
}
@@ -317,47 +376,13 @@ impl Tty {
device_list.remove(&node.dev_id());
// It hasn't been removed, update its state as usual.
let device = &self.devices[&node];
device.drm.activate();
// HACK: force reset the connectors to make resuming work across sleep.
let device = &self.devices[&node];
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
self.connector_disconnected(niri, node, conn, crtc);
}
let device = self.devices.get_mut(&node).unwrap();
let _ = device.drm_scanner.scan_connectors(&device.drm);
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
warn!("error connecting connector: {err:?}");
}
if let Err(err) = device.drm.activate(true) {
warn!("error activating DRM device: {err:?}");
}
// // Refresh the connectors.
// self.device_changed(node.dev_id(), niri);
// // Refresh the state on unchanged connectors.
// let device = self.devices.get_mut(&node).unwrap();
// for surface in device.surfaces.values_mut() {
// let compositor = &mut surface.compositor;
// if let Err(err) = compositor.surface().reset_state() {
// warn!("error resetting DRM surface state: {err}");
// }
// compositor.reset_buffers();
// }
// niri.queue_redraw_all();
// Refresh the connectors.
self.device_changed(node.dev_id(), niri);
}
// Add new devices.
@@ -366,6 +391,17 @@ impl Tty {
warn!("error adding device: {err:?}");
}
}
if self.update_output_config_on_resume {
self.on_output_config_changed(niri);
}
self.refresh_ipc_outputs();
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.monitors_active = true;
self.set_monitors_active(true);
niri.queue_redraw_all();
}
}
}
@@ -379,6 +415,8 @@ impl Tty {
debug!("device added: {device_id} {path:?}");
let node = DrmNode::from_dev_id(device_id)?;
let drm_lease_state = DrmLeaseState::new::<State>(&niri.display_handle, &node)
.context("Couldn't create DrmLeaseState")?;
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK;
let fd = self.session.open(path, open_flags)?;
@@ -387,15 +425,9 @@ impl Tty {
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
let gbm = GbmDevice::new(device_fd)?;
let display = EGLDisplay::new(gbm.clone())?;
let display = unsafe { EGLDisplay::new(gbm.clone())? };
let egl_device = EGLDevice::device_for_display(&display)?;
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
// same as the one we just created here, so when ours is dropped at the end of the scope,
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
// beforehand.
drop(display);
let render_node = egl_device
.try_get_render_node()?
.context("no render node")?;
@@ -474,6 +506,9 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
};
assert!(self.devices.insert(node, device).is_none());
@@ -506,12 +541,13 @@ impl Tty {
}
}
DrmScanEvent::Disconnected {
connector,
crtc: Some(crtc),
} => self.connector_disconnected(niri, node, connector, crtc),
crtc: Some(crtc), ..
} => self.connector_disconnected(niri, node, crtc),
_ => (),
}
}
self.refresh_ipc_outputs();
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -530,11 +566,11 @@ impl Tty {
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(info, crtc)| (info.clone(), crtc))
.map(|(_info, crtc)| crtc)
.collect();
for (connector, crtc) in crtcs {
self.connector_disconnected(niri, node, connector, crtc);
for crtc in crtcs {
self.connector_disconnected(niri, node, crtc);
}
let device = self.devices.remove(&node).unwrap();
@@ -576,6 +612,8 @@ impl Tty {
self.gpu_manager.as_mut().remove_node(&device.render_node);
niri.event_loop.remove(device.token);
self.refresh_ipc_outputs();
}
fn connector_connected(
@@ -592,6 +630,41 @@ impl Tty {
);
debug!("connecting connector: {output_name}");
let device = self.devices.get_mut(&node).context("missing device")?;
let non_desktop = device
.drm
.get_properties(connector.handle())
.ok()
.and_then(|props| {
let (info, value) = props
.into_iter()
.filter_map(|(handle, value)| {
let info = device.drm.get_property(handle).ok()?;
Some((info, value))
})
.find(|(info, _)| info.name().to_str() == Ok("non-desktop"))?;
info.value_type().convert_value(value).as_boolean()
})
.unwrap_or(false);
if non_desktop {
debug!("output is non desktop");
let description = EdidInfo::for_connector(&device.drm, connector.handle())
.map(|info| info.model)
.unwrap_or_else(|| "Unknown".into());
device.drm_lease_state.add_connector::<State>(
connector.handle(),
output_name,
description,
);
device
.non_desktop_connectors
.insert((connector.handle(), crtc));
return Ok(());
}
let config = self
.config
.borrow()
@@ -606,89 +679,30 @@ impl Tty {
return Ok(());
}
let device = self.devices.get_mut(&node).context("missing device")?;
// FIXME: print modes here until we have a better way to list all modes.
for m in connector.modes() {
let wl_mode = Mode::from(*m);
debug!(
"mode: {}x{}@{:.3}",
m.size().0,
m.size().1,
wl_mode.refresh as f64 / 1000.,
);
trace!("{m:?}");
}
let mut mode = None;
if let Some(target) = &config.mode {
let refresh = target.refresh.map(|r| (r * 1000.).round() as i32);
for m in connector.modes() {
if m.size() != (target.width, target.height) {
continue;
}
if let Some(refresh) = refresh {
// If refresh is set, only pick modes with matching refresh.
let wl_mode = Mode::from(*m);
if wl_mode.refresh == refresh {
mode = Some(m);
}
} else if let Some(curr) = mode {
// If refresh isn't set, pick the mode with the highest refresh.
if curr.vrefresh() < m.vrefresh() {
mode = Some(m);
}
let (mode, fallback) =
pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?;
if fallback {
let target = config.mode.unwrap();
warn!(
"configured mode {}x{}{} could not be found, falling back to preferred",
target.width,
target.height,
if let Some(refresh) = target.refresh {
format!("@{refresh}")
} else {
mode = Some(m);
}
}
if mode.is_none() {
warn!(
"configured mode {}x{}{} could not be found, falling back to preferred",
target.width,
target.height,
if let Some(refresh) = target.refresh {
format!("@{refresh}")
} else {
String::new()
},
);
}
String::new()
},
);
}
if mode.is_none() {
// Pick a preferred mode.
for m in connector.modes() {
if !m.mode_type().contains(ModeTypeFlags::PREFERRED) {
continue;
}
if let Some(curr) = mode {
if curr.vrefresh() < m.vrefresh() {
mode = Some(m);
}
} else {
mode = Some(m);
}
}
}
if mode.is_none() {
// Last attempt.
mode = connector.modes().first();
}
let mode = mode.ok_or_else(|| anyhow!("no mode"))?;
debug!("picking mode: {mode:?}");
let surface = device
.drm
.create_surface(crtc, *mode, &[connector.handle()])?;
.create_surface(crtc, mode, &[connector.handle()])?;
// Create GBM allocator.
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
@@ -711,9 +725,8 @@ impl Tty {
},
);
let wl_mode = Mode::from(*mode);
let scale = config.scale.clamp(1., 10.).ceil() as i32;
output.change_current_state(Some(wl_mode), None, Some(Scale::Integer(scale)), None);
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output
@@ -785,13 +798,8 @@ impl Tty {
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
self.connectors
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
let surface = Surface {
name: output_name,
name: output_name.clone(),
compositor,
dmabuf_feedback,
vblank_frame: None,
@@ -803,36 +811,39 @@ impl Tty {
let res = device.surfaces.insert(crtc, surface);
assert!(res.is_none(), "crtc must not have already existed");
niri.add_output(output.clone(), Some(refresh_interval(*mode)));
niri.add_output(output.clone(), Some(refresh_interval(mode)));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name, output.clone());
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&state.backend);
state.niri.activate_monitors(&mut state.backend);
state.niri.queue_redraw(output);
});
Ok(())
}
fn connector_disconnected(
&mut self,
niri: &mut Niri,
node: DrmNode,
connector: connector::Info,
crtc: crtc::Handle,
) {
debug!("disconnecting connector: {connector:?}");
fn connector_disconnected(&mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle) {
let Some(device) = self.devices.get_mut(&node) else {
debug!("disconnecting connector for crtc: {crtc:?}");
error!("missing device");
return;
};
let Some(surface) = device.surfaces.remove(&crtc) else {
debug!("disconnecting connector for crtc: {crtc:?}");
debug!("crtc wasn't enabled");
return;
};
debug!("disconnecting connector: {:?}", surface.name);
let output = niri
.global_space
.outputs()
@@ -847,7 +858,9 @@ impl Tty {
error!("missing output for crtc {crtc:?}");
};
self.connectors.lock().unwrap().remove(&surface.name);
self.enabled_outputs.lock().unwrap().remove(&surface.name);
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
}
fn on_vblank(
@@ -1093,7 +1106,7 @@ impl Tty {
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
@@ -1112,7 +1125,7 @@ impl Tty {
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
}
if res.damage.is_some() {
if !res.is_empty {
let presentation_feedbacks =
niri.take_presentation_feedbacks(output, &res.states);
let data = (presentation_feedbacks, target_presentation_time);
@@ -1180,20 +1193,20 @@ impl Tty {
}
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) {
Ok(renderer) => renderer,
Err(err) => {
debug!("error creating renderer for primary GPU: {err:?}");
return Err(());
return false;
}
};
match renderer.import_dmabuf(dmabuf, None) {
Ok(_texture) => Ok(()),
Ok(_texture) => true,
Err(err) => {
debug!("error importing dmabuf: {err:?}");
Err(())
false
}
}
}
@@ -1210,8 +1223,66 @@ impl Tty {
}
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
fn refresh_ipc_outputs(&self) {
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
let mut ipc_outputs = HashMap::new();
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let physical_size = connector.size();
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
.map(|info| (info.manufacturer, info.model))
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let modes = connector
.modes()
.iter()
.map(|m| niri_ipc::Mode {
width: m.size().0,
height: m.size().1,
refresh_rate: Mode::from(*m).refresh as u32,
})
.collect();
let mut output = niri_ipc::Output {
name: connector_name.clone(),
make,
model,
physical_size,
modes,
current_mode: None,
};
if let Some(surface) = device.surfaces.get(&crtc) {
let current = surface.compositor.pending_mode();
if let Some(current) = connector.modes().iter().position(|m| *m == current) {
output.current_mode = Some(current);
} else {
error!("connector mode list missing current mode");
}
}
ipc_outputs.insert(connector_name, output);
}
}
self.ipc_outputs.replace(ipc_outputs);
}
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()
}
#[cfg(feature = "xdp-gnome-screencast")]
@@ -1219,13 +1290,170 @@ impl Tty {
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
}
pub fn set_monitors_active(&self, active: bool) {
for device in self.devices.values() {
for crtc in device.surfaces.keys() {
set_crtc_active(&device.drm, *crtc, active);
pub fn set_monitors_active(&mut self, active: bool) {
// We only disable the CRTC here, this will also reset the
// surface state so that the next call to `render_frame` will
// always produce a new frame and `queue_frame` will change
// the CRTC to active. This makes sure we always enable a CRTC
// within an atomic operation.
if active {
return;
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
}
}
}
}
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::on_output_config_changed");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_output_config_on_resume = true;
return;
}
self.update_output_config_on_resume = false;
let mut to_disconnect = vec![];
let mut to_connect = vec![];
for (&node, device) in &mut self.devices {
for surface in device.surfaces.values_mut() {
let crtc = surface.compositor.crtc();
let config = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name == surface.name)
.cloned()
.unwrap_or_default();
if config.off {
to_disconnect.push((node, crtc));
continue;
}
// Check if we need to change the mode.
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
};
let Some((mode, fallback)) = pick_mode(connector, config.mode) else {
error!("couldn't pick mode for enabled connector");
continue;
};
if surface.compositor.pending_mode() == mode {
continue;
}
let output = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == node && tty_state.crtc == crtc
})
.cloned();
let Some(output) = output else {
error!("missing output for crtc: {crtc:?}");
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
continue;
};
if fallback {
let target = config.mode.unwrap();
warn!(
"output {:?}: configured mode {}x{}{} could not be found, \
falling back to preferred",
surface.name,
target.width,
target.height,
if let Some(refresh) = target.refresh {
format!("@{refresh}")
} else {
String::new()
},
);
}
debug!("output {:?}: picking mode: {mode:?}", surface.name);
if let Err(err) = surface.compositor.use_mode(mode) {
warn!("error changing mode: {err:?}");
continue;
}
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output_state.frame_clock = FrameClock::new(Some(refresh_interval(mode)));
niri.output_resized(output);
}
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
continue;
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
continue;
}
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let config = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name == output_name)
.cloned()
.unwrap_or_default();
if !config.off {
to_connect.push((node, connector.clone(), crtc));
}
}
}
for (node, crtc) in to_disconnect {
self.connector_disconnected(niri, node, crtc);
}
for (node, connector, crtc) in to_connect {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
}
self.refresh_ipc_outputs();
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
}
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
@@ -1365,9 +1593,17 @@ fn refresh_interval(mode: DrmMode) -> Duration {
#[cfg(feature = "dbus")]
fn suspend() -> anyhow::Result<()> {
let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?;
let manager = logind_zbus::manager::ManagerProxyBlocking::new(&conn)
.context("error creating login manager proxy")?;
manager.suspend(true).context("error suspending")
conn.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Suspend",
&(true),
)
.context("error suspending")?;
Ok(())
}
fn queue_estimated_vblank_timer(
@@ -1400,3 +1636,64 @@ fn queue_estimated_vblank_timer(
.unwrap();
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
}
fn pick_mode(
connector: &connector::Info,
target: Option<niri_config::Mode>,
) -> Option<(control::Mode, bool)> {
let mut mode = None;
let mut fallback = false;
if let Some(target) = target {
let refresh = target.refresh.map(|r| (r * 1000.).round() as i32);
for m in connector.modes() {
if m.size() != (target.width, target.height) {
continue;
}
if let Some(refresh) = refresh {
// If refresh is set, only pick modes with matching refresh.
let wl_mode = Mode::from(*m);
if wl_mode.refresh == refresh {
mode = Some(m);
}
} else if let Some(curr) = mode {
// If refresh isn't set, pick the mode with the highest refresh.
if curr.vrefresh() < m.vrefresh() {
mode = Some(m);
}
} else {
mode = Some(m);
}
}
if mode.is_none() {
fallback = true;
}
}
if mode.is_none() {
// Pick a preferred mode.
for m in connector.modes() {
if !m.mode_type().contains(ModeTypeFlags::PREFERRED) {
continue;
}
if let Some(curr) = mode {
if curr.vrefresh() < m.vrefresh() {
mode = Some(m);
}
} else {
mode = Some(m);
}
}
}
if mode.is_none() {
// Last attempt.
mode = connector.modes().first();
}
mode.map(|m| (*m, fallback))
}
+48 -35
View File
@@ -11,41 +11,35 @@ use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
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::niri::{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(),
@@ -61,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(),
)])));
@@ -90,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),
@@ -97,20 +108,18 @@ 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) {
@@ -202,17 +211,21 @@ impl Winit {
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self.backend.renderer().import_dmabuf(dmabuf, None) {
Ok(_texture) => Ok(()),
Ok(_texture) => true,
Err(err) => {
debug!("error importing dmabuf: {err:?}");
Err(())
false
}
}
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
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,
},
}
+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)
}
+27 -4
View File
@@ -97,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;
@@ -125,7 +144,11 @@ impl CompositorHandler for State {
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if !is_mapped {
// The toplevel got unmapped.
+185 -12
View File
@@ -9,10 +9,13 @@ use std::sync::Arc;
use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::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;
@@ -20,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,
@@ -36,13 +48,18 @@ 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::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 {
@@ -70,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);
@@ -186,6 +216,11 @@ impl DataControlHandler for State {
delegate_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
delegate_presentation!(State);
@@ -201,13 +236,10 @@ impl DmabufHandler for State {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
match self.backend.import_dmabuf(&dmabuf) {
Ok(_) => {
let _ = notifier.successful::<State>();
}
Err(_) => {
notifier.failed();
}
if self.backend.import_dmabuf(&dmabuf) {
let _ = notifier.successful::<State>();
} else {
notifier.failed();
}
}
}
@@ -251,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);
+135 -33
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,7 @@ 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 {
@@ -90,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) {
@@ -116,9 +214,6 @@ 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
@@ -135,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);
});
}
}
}
@@ -151,6 +253,13 @@ impl XdgShellHandler for State {
{
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);
});
}
}
}
@@ -192,49 +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 {
zxdg_toplevel_decoration_v1::Mode::ServerSide
} else {
zxdg_toplevel_decoration_v1::Mode::ClientSide
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(mode);
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
}
fn request_mode(
&mut self,
toplevel: ToplevelSurface,
mut mode: zxdg_toplevel_decoration_v1::Mode,
) {
// If prefer-no-csd is unset, then insist on CSD.
if !self.niri.config.borrow().prefer_no_csd {
mode = zxdg_toplevel_decoration_v1::Mode::ClientSide;
}
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 {
zxdg_toplevel_decoration_v1::Mode::ServerSide
} else {
zxdg_toplevel_decoration_v1::Mode::ClientSide
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(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();
}
}
}
+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()
}
}
+468 -33
View File
@@ -1,7 +1,8 @@
use std::any::Any;
use std::collections::HashSet;
use niri_config::{Action, Binds, LayoutAction, Modifiers};
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 _,
@@ -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,
@@ -221,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 => {
@@ -241,7 +308,7 @@ impl State {
self.niri.suppressed_keys.clear();
}
Action::PowerOffMonitors => {
self.niri.deactivate_monitors(&self.backend);
self.niri.deactivate_monitors(&mut self.backend);
}
Action::ToggleDebugTint => {
self.backend.toggle_debug_tint();
@@ -311,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(),
},
);
}
@@ -362,23 +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();
@@ -406,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
@@ -501,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);
}
}
}
}
@@ -754,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();
}
};
@@ -1024,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);
@@ -1255,6 +1430,15 @@ fn action(
_ => (),
}
bound_action(bindings, comp_mod, raw, mods)
}
fn bound_action(
bindings: &Binds,
comp_mod: CompositorMod,
raw: Option<Keysym>,
mods: ModifiersState,
) -> Option<Action> {
// Handle configured binds.
let mut modifiers = Modifiers::empty();
if mods.ctrl {
@@ -1270,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();
}
}
@@ -1318,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
@@ -1332,10 +1556,66 @@ 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};
@@ -1463,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)
}
+711 -52
View File
File diff suppressed because it is too large Load Diff
+122 -15
View File
@@ -2,20 +2,19 @@ use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_config::SizeChange;
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
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)]
@@ -97,7 +96,8 @@ impl<W: LayoutElement> Monitor<W> {
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
current_idx,
idx as f64,
Duration::from_millis(250),
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
}
@@ -127,7 +127,46 @@ impl<W: LayoutElement> Monitor<W> {
}
}
fn clean_up_workspaces(&mut self) {
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() {
@@ -197,6 +236,14 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn consume_or_expel_window_left(&mut self) {
self.active_workspace().consume_or_expel_window_left();
}
pub fn consume_or_expel_window_right(&mut self) {
self.active_workspace().consume_or_expel_window_right();
}
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
}
@@ -323,6 +370,62 @@ impl<W: LayoutElement> Monitor<W> {
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));
}
@@ -503,16 +606,11 @@ impl<W: LayoutElement> Monitor<W> {
let ws = &self.workspaces[self.active_workspace_idx];
ws.render_above_top_layer()
}
}
impl Monitor<Window> {
pub fn render_elements<R: Renderer + ImportAll>(
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> Vec<MonitorRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
@@ -531,12 +629,18 @@ impl Monitor<Window> {
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((0, offset), (size.w, size.h)),
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, size.h),
),
)?,
(0, -offset),
Relocate::Relative,
@@ -547,7 +651,10 @@ impl Monitor<Window> {
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities((0, 0), (size.w, offset)),
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
+150 -34
View File
@@ -3,14 +3,18 @@ use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::backend::renderer::element::utils::{
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::workspace::WorkspaceRenderElement;
use super::{LayoutElement, Options};
use super::{LayoutElement, LayoutElementRenderElement, Options};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -21,6 +25,12 @@ pub struct Tile<W: LayoutElement> {
/// The border around the window.
border: FocusRing,
/// The focus ring around the window.
///
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
/// currently here.
focus_ring: FocusRing,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
@@ -33,24 +43,38 @@ pub struct Tile<W: LayoutElement> {
/// The size we were requested to fullscreen into.
fullscreen_size: Size<i32, Logical>,
/// The animation upon opening a window.
open_animation: Option<Animation>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
niri_render_elements! {
TileRenderElement => {
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),
border: FocusRing::new(options.border.into()),
focus_ring: FocusRing::new(options.focus_ring),
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
fullscreen_size: Default::default(),
open_animation: None,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
self.border.update_config(options.border);
self.border.update_config(options.border.into());
self.focus_ring.update_config(options.focus_ring);
self.options = options;
}
@@ -61,7 +85,7 @@ impl<W: LayoutElement> Tile<W> {
}
}
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
let width = self.border.width();
self.border.update(
(width, width).into(),
@@ -69,6 +93,33 @@ impl<W: LayoutElement> Tile<W> {
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 {
@@ -141,6 +192,22 @@ impl<W: LayoutElement> Tile<W> {
self.window.size()
}
/// Returns an animated size of the tile for rendering and input.
///
/// During the window opening animation, windows to the right should gradually slide further to
/// the right. This is what this visual size is used for. Other things like window resizes or
/// transactions or new view position calculation always use the real size, instead of this
/// visual size.
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
let size = self.tile_size();
let v = self
.open_animation
.as_ref()
.map(|anim| anim.value())
.unwrap_or(1.);
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
}
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
loc += self.window_loc();
@@ -232,36 +299,44 @@ impl<W: LayoutElement> Tile<W> {
self.effective_border_width().is_some() || self.window.has_ssd()
}
pub fn render<R: Renderer + ImportAll>(
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<WorkspaceRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
let mut rv = Vec::new();
focus_ring: bool,
) -> impl Iterator<Item = TileRenderElement<R>> {
let rv = self
.window
.render(renderer, location + self.window_loc(), scale)
.into_iter()
.map(Into::into);
let window_pos = location + self.window_loc();
rv.extend(self.window.render(renderer, window_pos, scale));
let elem = self.effective_border_width().map(|_| {
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());
if self.effective_border_width().is_some() {
rv.extend(
self.border
.render(scale)
.map(|elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
})
.map(Into::into),
);
}
let elem = focus_ring.then(|| {
self.focus_ring.render(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());
if self.is_fullscreen {
let elem = self.is_fullscreen.then(|| {
let elem = SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
@@ -269,9 +344,50 @@ impl<W: LayoutElement> Tile<W> {
1.,
Kind::Unspecified,
);
rv.push(elem.into());
}
RelocateRenderElement::from_element(elem, (0, 0), Relocate::Relative).into()
});
rv.chain(elem)
}
rv
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())
}
}
}
+421 -187
View File
@@ -1,23 +1,21 @@
use std::cmp::{max, min};
use std::iter::zip;
use std::iter::{self, zip};
use std::rc::Rc;
use std::time::Duration;
use niri_config::{PresetWidth, SizeChange, Struts};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::utils::RelocateRenderElement;
use smithay::backend::renderer::{ImportAll, Renderer};
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
use niri_ipc::SizeChange;
use smithay::desktop::space::SpaceElement;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::render_elements;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::tile::Tile;
use super::tile::{Tile, TileRenderElement};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::output_size;
#[derive(Debug)]
@@ -49,9 +47,6 @@ pub struct Workspace<W: LayoutElement> {
/// Index of the currently active column, if any.
pub active_column_idx: usize,
/// Focus ring buffer and parameters.
focus_ring: FocusRing,
/// Offset of the view computed from the active column.
///
/// Any gaps, including left padding from work area left exclusive zone, is handled
@@ -79,12 +74,10 @@ pub struct Workspace<W: LayoutElement> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputId(String);
render_elements! {
#[derive(Debug)]
pub WorkspaceRenderElement<R> where R: ImportAll;
Wayland = WaylandSurfaceRenderElement<R>,
FocusRing = FocusRingRenderElement,
Border = RelocateRenderElement<FocusRingRenderElement>,
niri_render_elements! {
WorkspaceRenderElement => {
Tile = TileRenderElement<R>,
}
}
/// Width of a column.
@@ -197,7 +190,6 @@ impl<W: LayoutElement> Workspace<W> {
output: Some(output),
columns: vec![],
active_column_idx: 0,
focus_ring: FocusRing::new(options.focus_ring),
view_offset: 0,
view_offset_anim: None,
activate_prev_column_on_removal: false,
@@ -213,7 +205,6 @@ impl<W: LayoutElement> Workspace<W> {
working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)),
columns: vec![],
active_column_idx: 0,
focus_ring: FocusRing::new(options.focus_ring),
view_offset: 0,
view_offset_anim: None,
activate_prev_column_on_removal: false,
@@ -233,42 +224,17 @@ impl<W: LayoutElement> Workspace<W> {
None => (),
}
let view_pos = self.view_pos();
for (col_idx, col) in self.columns.iter_mut().enumerate() {
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
let is_active = is_active
&& col_idx == self.active_column_idx
&& tile_idx == col.active_tile_idx;
tile.advance_animations(current_time, is_active);
}
}
// This shall one day become a proper animation.
if !self.columns.is_empty() {
let col = &self.columns[self.active_column_idx];
let active_tile = &col.tiles[col.active_tile_idx];
let size = active_tile.tile_size();
let has_ssd = active_tile.has_ssd();
let tile_pos = Point::from((
self.column_x(self.active_column_idx) - view_pos,
col.tile_y(col.active_tile_idx),
));
self.focus_ring.update(tile_pos, size, has_ssd);
self.focus_ring.set_active(is_active);
let is_active = is_active && col_idx == self.active_column_idx;
col.advance_animations(current_time, is_active);
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.view_offset_anim.is_some()
self.view_offset_anim.is_some() || self.columns.iter().any(Column::are_animations_ongoing)
}
pub fn update_config(&mut self, options: Rc<Options>) {
self.focus_ring.update_config(options.focus_ring);
// The focus ring buffer will be updated in a subsequent update_animations call.
for column in &mut self.columns {
column.update_config(options.clone());
}
@@ -308,7 +274,7 @@ impl<W: LayoutElement> Workspace<W> {
fn enter_output_for_window(&self, window: &W) {
if let Some(output) = &self.output {
prepare_for_output(window, output);
set_preferred_scale_transform(window, output);
window.output_enter(output);
}
}
@@ -330,6 +296,19 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn view_size(&self) -> Size<i32, Logical> {
self.view_size
}
pub fn update_output_scale_transform(&mut self) {
let Some(output) = self.output.as_ref() else {
return;
};
for window in self.windows() {
set_preferred_scale_transform(window, output);
}
}
fn toplevel_bounds(&self) -> Size<i32, Logical> {
let mut border = 0;
if !self.options.border.off {
@@ -342,7 +321,7 @@ impl<W: LayoutElement> Workspace<W> {
))
}
pub fn configure_new_window(&self, window: &Window) {
pub fn new_window_size(&self) -> Size<i32, Logical> {
let width = if let Some(width) = self.options.default_width {
let mut width = width.resolve(&self.options, self.working_area.size.w);
if !self.options.border.off {
@@ -358,12 +337,15 @@ impl<W: LayoutElement> Workspace<W> {
height -= self.options.border.width as i32 * 2;
}
let size = Size::from((width, max(height, 1)));
Size::from((width, max(height, 1)))
}
pub fn configure_new_window(&self, window: &Window) {
let size = self.new_window_size();
let bounds = self.toplevel_bounds();
if let Some(output) = self.output.as_ref() {
prepare_for_output(window, output);
set_preferred_scale_transform(window, output);
}
window.toplevel().with_pending_state(|state| {
@@ -397,9 +379,7 @@ impl<W: LayoutElement> Workspace<W> {
new_offset - self.working_area.loc.x
}
fn animate_view_offset_to_column(&mut self, current_x: i32, idx: usize) {
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
fn animate_view_offset(&mut self, current_x: i32, idx: usize, new_view_offset: i32) {
let new_col_x = self.column_x(idx);
let from_view_offset = current_x - new_col_x;
self.view_offset = from_view_offset;
@@ -422,17 +402,83 @@ impl<W: LayoutElement> Workspace<W> {
self.view_offset_anim = Some(Animation::new(
self.view_offset as f64,
new_view_offset as f64,
Duration::from_millis(250),
self.options.animations.horizontal_view_movement,
niri_config::Animation::default_horizontal_view_movement(),
));
}
fn animate_view_offset_to_column(&mut self, current_x: i32, idx: usize) {
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
self.animate_view_offset(current_x, idx, new_view_offset);
}
fn animate_view_offset_to_column_centered(&mut self, current_x: i32, idx: usize) {
if self.columns.is_empty() {
return;
}
let col = &self.columns[idx];
if col.is_fullscreen {
self.animate_view_offset_to_column(current_x, idx);
return;
}
let width = col.width();
// If the column is wider than the working area, then on commit it will be shifted to left
// edge alignment by the usual positioning code, so there's no use in trying to center it
// here.
if self.working_area.size.w <= width {
self.animate_view_offset_to_column(current_x, idx);
return;
}
let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
self.animate_view_offset(current_x, idx, new_view_offset);
}
fn activate_column(&mut self, idx: usize) {
if self.active_column_idx == idx {
return;
}
let current_x = self.view_pos();
self.animate_view_offset_to_column(current_x, idx);
match self.options.center_focused_column {
CenterFocusedColumn::Always => {
self.animate_view_offset_to_column_centered(current_x, idx)
}
CenterFocusedColumn::OnOverflow => {
// Always take the left or right neighbor of the target as the source.
let source_idx = if self.active_column_idx > idx {
min(idx + 1, self.columns.len() - 1)
} else {
idx.saturating_sub(1)
};
let source_x = self.column_x(source_idx);
let source_width = self.columns[source_idx].width();
let target_x = self.column_x(idx);
let target_width = self.columns[idx].width();
let total_width = if source_x < target_x {
// Source is left from target.
target_x - source_x + target_width
} else {
// Source is right from target.
source_x - target_x + source_width
} + self.options.gaps * 2;
// If it fits together, do a normal animation, otherwise center the new column.
if total_width <= self.working_area.size.w {
self.animate_view_offset_to_column(current_x, idx);
} else {
self.animate_view_offset_to_column_centered(current_x, idx);
}
}
CenterFocusedColumn::Never => self.animate_view_offset_to_column(current_x, idx),
};
self.active_column_idx = idx;
@@ -463,6 +509,16 @@ impl<W: LayoutElement> Workspace<W> {
x
}
fn visual_column_x(&self, column_idx: usize) -> i32 {
let mut x = 0;
for column in self.columns.iter().take(column_idx) {
x += column.visual_width() + self.options.gaps;
}
x
}
pub fn add_window(
&mut self,
window: W,
@@ -488,15 +544,93 @@ impl<W: LayoutElement> Workspace<W> {
width,
is_full_width,
);
let width = column.width();
self.columns.insert(idx, column);
if activate {
// If this is the first window on an empty workspace, skip the animation from whatever
// view_offset was left over.
if was_empty {
// Try to make the code produce a left-aligned offset, even in presence of left
// exclusive zones.
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
if self.options.center_focused_column == CenterFocusedColumn::Always {
self.view_offset =
-(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
} else {
// Try to make the code produce a left-aligned offset, even in presence of left
// exclusive zones.
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
}
self.view_offset_anim = None;
}
self.activate_column(idx);
self.activate_prev_column_on_removal = true;
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
self.enter_output_for_window(&window);
let right_of_idx = self
.columns
.iter()
.position(|col| col.contains(right_of))
.unwrap();
let idx = right_of_idx + 1;
let column = Column::new(
window,
self.view_size,
self.working_area,
self.options.clone(),
width,
is_full_width,
);
self.columns.insert(idx, column);
// Activate the new window if right_of was active.
if self.active_column_idx == right_of_idx {
self.activate_column(idx);
self.activate_prev_column_on_removal = true;
} else if idx <= self.active_column_idx {
self.active_column_idx += 1;
}
}
pub fn add_column(&mut self, mut column: Column<W>, activate: bool) {
for tile in &column.tiles {
self.enter_output_for_window(tile.window());
}
let was_empty = self.columns.is_empty();
let idx = if self.columns.is_empty() {
0
} else {
self.active_column_idx + 1
};
column.set_view_size(self.view_size, self.working_area);
let width = column.width();
self.columns.insert(idx, column);
if activate {
// If this is the first window on an empty workspace, skip the animation from whatever
// view_offset was left over.
if was_empty {
if self.options.center_focused_column == CenterFocusedColumn::Always {
self.view_offset =
-(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
} else {
// Try to make the code produce a left-aligned offset, even in presence of left
// exclusive zones.
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
}
self.view_offset_anim = None;
}
@@ -549,6 +683,42 @@ impl<W: LayoutElement> Workspace<W> {
window
}
pub fn remove_column_by_idx(&mut self, column_idx: usize) -> Column<W> {
let column = self.columns.remove(column_idx);
if let Some(output) = &self.output {
for tile in &column.tiles {
tile.window().output_leave(output);
}
}
if column_idx + 1 == self.active_column_idx {
// The previous column, that we were going to activate upon removal of the active
// column, has just been itself removed.
self.activate_prev_column_on_removal = false;
}
// FIXME: activate_column below computes current view position to compute the new view
// position, which can include the column we're removing here. This leads to unwanted
// view jumps.
if self.columns.is_empty() {
return column;
}
if self.active_column_idx > column_idx
|| (self.active_column_idx == column_idx && self.activate_prev_column_on_removal)
{
// A column to the left was removed; preserve the current position.
// FIXME: preserve activate_prev_column_on_removal.
// Or, the active column was removed, and we needed to activate the previous column.
self.activate_column(self.active_column_idx.saturating_sub(1));
} else {
self.activate_column(min(self.active_column_idx, self.columns.len() - 1));
}
column
}
pub fn remove_window(&mut self, window: &W) {
let column_idx = self
.columns
@@ -574,7 +744,14 @@ impl<W: LayoutElement> Workspace<W> {
if idx == self.active_column_idx {
// We might need to move the view to ensure the resized window is still visible.
let current_x = self.view_pos();
self.animate_view_offset_to_column(current_x, idx);
if self.options.center_focused_column == CenterFocusedColumn::Always {
// FIXME: we will want to skip the animation in some cases here to make
// continuously resizing windows not look janky.
self.animate_view_offset_to_column_centered(current_x, idx);
} else {
self.animate_view_offset_to_column(current_x, idx);
}
}
}
@@ -654,6 +831,7 @@ impl<W: LayoutElement> Workspace<W> {
let column = self.columns.remove(self.active_column_idx);
self.columns.insert(new_idx, column);
// FIXME: should this be different when always centering?
self.view_offset =
self.compute_new_view_offset_for_column(current_x, self.active_column_idx);
@@ -703,6 +881,70 @@ impl<W: LayoutElement> Workspace<W> {
self.columns[self.active_column_idx].move_up();
}
pub fn consume_or_expel_window_left(&mut self) {
if self.columns.is_empty() {
return;
}
let source_column = &self.columns[self.active_column_idx];
if source_column.tiles.len() == 1 {
if self.active_column_idx == 0 {
return;
}
// Move into adjacent column.
let target_column_idx = self.active_column_idx - 1;
let window = self.remove_window_by_idx(self.active_column_idx, 0);
self.enter_output_for_window(&window);
let target_column = &mut self.columns[target_column_idx];
target_column.add_window(window);
target_column.focus_last();
self.activate_column(target_column_idx);
} else {
// Move out of column.
let width = source_column.width;
let is_full_width = source_column.is_full_width;
let window =
self.remove_window_by_idx(self.active_column_idx, source_column.active_tile_idx);
self.add_window(window, true, width, is_full_width);
// Window was added to the right of current column, so move the new column left.
self.move_left();
}
}
pub fn consume_or_expel_window_right(&mut self) {
if self.columns.is_empty() {
return;
}
let source_column = &self.columns[self.active_column_idx];
if source_column.tiles.len() == 1 {
if self.active_column_idx + 1 == self.columns.len() {
return;
}
// Move into adjacent column.
let target_column_idx = self.active_column_idx;
let window = self.remove_window_by_idx(self.active_column_idx, 0);
self.enter_output_for_window(&window);
let target_column = &mut self.columns[target_column_idx];
target_column.add_window(window);
target_column.focus_last();
self.activate_column(target_column_idx);
} else {
// Move out of column.
let width = source_column.width;
let is_full_width = source_column.is_full_width;
let window =
self.remove_window_by_idx(self.active_column_idx, source_column.active_tile_idx);
self.add_window(window, true, width, is_full_width);
}
}
pub fn consume_into_column(&mut self) {
if self.columns.len() < 2 {
return;
@@ -714,6 +956,7 @@ impl<W: LayoutElement> Workspace<W> {
let source_column_idx = self.active_column_idx + 1;
let window = self.remove_window_by_idx(source_column_idx, 0);
self.enter_output_for_window(&window);
let target_column = &mut self.columns[self.active_column_idx];
target_column.add_window(window);
@@ -738,48 +981,53 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn center_column(&mut self) {
if self.columns.is_empty() {
return;
}
let col = &self.columns[self.active_column_idx];
if col.is_fullscreen {
return;
}
let width = col.width();
// If the column is wider than the working area, then on commit it will be shifted to left
// edge alignment by the usual positioning code, so there's no use in doing anything here.
if self.working_area.size.w <= width {
return;
}
let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
// If we're already animating towards that, don't restart it.
if let Some(anim) = &self.view_offset_anim {
if anim.to().round() as i32 == new_view_offset {
return;
}
}
// If our view offset is already this, we don't need to do anything.
if self.view_offset == new_view_offset {
return;
}
self.view_offset_anim = Some(Animation::new(
self.view_offset as f64,
new_view_offset as f64,
Duration::from_millis(250),
));
let center_x = self.view_pos();
self.animate_view_offset_to_column_centered(center_x, self.active_column_idx);
}
fn view_pos(&self) -> i32 {
self.column_x(self.active_column_idx) + self.view_offset
}
fn tiles_in_render_order(&self) -> impl Iterator<Item = (&'_ Tile<W>, Point<i32, Logical>)> {
let view_pos = self.visual_column_x(self.active_column_idx) + self.view_offset;
// Start with the active window since it's drawn on top.
let col = &self.columns[self.active_column_idx];
let tile = &col.tiles[col.active_tile_idx];
let tile_pos = Point::from((
self.visual_column_x(self.active_column_idx) - view_pos,
col.tile_y(col.active_tile_idx),
));
let first = iter::once((tile, tile_pos));
let mut x = -view_pos;
let rest = self
.columns
.iter()
.enumerate()
// Keep track of column X position.
.map(move |(col_idx, col)| {
let rv = (col_idx, col, x);
x += col.visual_width() + self.options.gaps;
rv
})
.flat_map(move |(col_idx, col, x)| {
zip(&col.tiles, col.tile_ys()).enumerate().filter_map(
move |(tile_idx, (tile, y))| {
if col_idx == self.active_column_idx && tile_idx == col.active_tile_idx {
// Active tile comes first.
return None;
}
let tile_pos = Point::from((x, y));
Some((tile, tile_pos))
},
)
});
first.chain(rest)
}
pub fn window_under(
&self,
pos: Point<f64, Logical>,
@@ -788,45 +1036,18 @@ impl<W: LayoutElement> Workspace<W> {
return None;
}
let view_pos = self.view_pos();
self.tiles_in_render_order().find_map(|(tile, tile_pos)| {
let pos_within_tile = pos - tile_pos.to_f64();
// Prefer the active window since it's drawn on top.
let col = &self.columns[self.active_column_idx];
let active_tile = &col.tiles[col.active_tile_idx];
let tile_pos = Point::from((
self.column_x(self.active_column_idx) - view_pos,
col.tile_y(col.active_tile_idx),
));
let pos_within_tile = pos - tile_pos.to_f64();
if active_tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + active_tile.buf_loc();
return Some((active_tile.window(), Some(pos_within_surface)));
} else if active_tile.is_in_activation_region(pos_within_tile) {
return Some((active_tile.window(), None));
}
let mut x = -view_pos;
for col in &self.columns {
for (tile, y) in zip(&col.tiles, col.tile_ys()) {
if tile.window() == active_tile.window() {
// Already handled it above.
continue;
}
let tile_pos = Point::from((x, y));
let pos_within_tile = pos - tile_pos.to_f64();
if tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + tile.buf_loc();
return Some((tile.window(), Some(pos_within_surface)));
} else if tile.is_in_activation_region(pos_within_tile) {
return Some((tile.window(), None));
}
if tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + tile.buf_loc();
return Some((tile.window(), Some(pos_within_surface)));
} else if tile.is_in_activation_region(pos_within_tile) {
return Some((tile.window(), None));
}
x += col.width() + self.options.gaps;
}
None
None
})
}
pub fn toggle_width(&mut self) {
@@ -925,6 +1146,39 @@ impl<W: LayoutElement> Workspace<W> {
self.columns[self.active_column_idx].is_fullscreen
}
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> Vec<WorkspaceRenderElement<R>> {
if self.columns.is_empty() {
return vec![];
}
// FIXME: workspaces should probably cache their last used scale so they can be correctly
// rendered even with no outputs connected.
let output_scale = self
.output
.as_ref()
.map(|o| Scale::from(o.current_scale().fractional_scale()))
.unwrap_or(Scale::from(1.));
let mut rv = vec![];
let mut first = true;
for (tile, tile_pos) in self.tiles_in_render_order() {
// For the active tile (which comes first), draw the focus ring.
let focus_ring = first;
first = false;
rv.extend(
tile.render(renderer, tile_pos, output_scale, focus_ring)
.map(Into::into),
);
}
rv
}
}
impl Workspace<Window> {
@@ -948,60 +1202,6 @@ impl Workspace<Window> {
}
}
}
pub fn render_elements<R: Renderer + ImportAll>(
&self,
renderer: &mut R,
) -> Vec<WorkspaceRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
if self.columns.is_empty() {
return vec![];
}
// FIXME: workspaces should probably cache their last used scale so they can be correctly
// rendered even with no outputs connected.
let output_scale = self
.output
.as_ref()
.map(|o| Scale::from(o.current_scale().fractional_scale()))
.unwrap_or(Scale::from(1.));
let mut rv = vec![];
let view_pos = self.view_pos();
// Draw the active window on top.
let col = &self.columns[self.active_column_idx];
let active_tile = &col.tiles[col.active_tile_idx];
let tile_pos = Point::from((
self.column_x(self.active_column_idx) - view_pos,
col.tile_y(col.active_tile_idx),
));
// Draw the window itself.
rv.extend(active_tile.render(renderer, tile_pos, output_scale));
// Draw the focus ring.
rv.extend(self.focus_ring.render(output_scale).map(Into::into));
let mut x = -view_pos;
for col in &self.columns {
for (tile, y) in zip(&col.tiles, col.tile_ys()) {
if tile.window() == active_tile.window() {
// Already handled it above.
continue;
}
let tile_pos = Point::from((x, y));
rv.extend(tile.render(renderer, tile_pos, output_scale));
}
x += col.width() + self.options.gaps;
}
rv
}
}
impl<W: LayoutElement> Column<W> {
@@ -1025,8 +1225,14 @@ impl<W: LayoutElement> Column<W> {
options,
};
let is_pending_fullscreen = window.is_pending_fullscreen();
rv.add_window(window);
if is_pending_fullscreen {
rv.set_fullscreen(true);
}
rv
}
@@ -1078,6 +1284,17 @@ impl<W: LayoutElement> Column<W> {
self.update_tile_sizes();
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
for (tile_idx, tile) in self.tiles.iter_mut().enumerate() {
let is_active = is_active && tile_idx == self.active_tile_idx;
tile.advance_animations(current_time, is_active);
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.tiles.iter().any(Tile::are_animations_ongoing)
}
pub fn contains(&self, window: &W) -> bool {
self.tiles.iter().map(Tile::window).any(|win| win == window)
}
@@ -1276,6 +1493,14 @@ impl<W: LayoutElement> Column<W> {
.unwrap()
}
fn visual_width(&self) -> i32 {
self.tiles
.iter()
.map(|tile| tile.visual_tile_size().w)
.max()
.unwrap()
}
fn focus_up(&mut self) {
self.active_tile_idx = self.active_tile_idx.saturating_sub(1);
}
@@ -1284,6 +1509,10 @@ impl<W: LayoutElement> Column<W> {
self.active_tile_idx = min(self.active_tile_idx + 1, self.tiles.len() - 1);
}
fn focus_last(&mut self) {
self.active_tile_idx = self.tiles.len() - 1;
}
fn move_up(&mut self) {
let new_idx = self.active_tile_idx.saturating_sub(1);
if self.active_tile_idx == new_idx {
@@ -1315,6 +1544,10 @@ impl<W: LayoutElement> Column<W> {
if self.is_fullscreen {
assert_eq!(self.tiles.len(), 1);
}
for tile in &self.tiles {
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
}
}
fn toggle_width(&mut self) {
@@ -1509,7 +1742,8 @@ fn compute_new_view_offset(
}
}
fn prepare_for_output(window: &impl LayoutElement, output: &Output) {
fn set_preferred_scale_transform(window: &impl LayoutElement, output: &Output) {
// FIXME: cache this on the workspace.
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
window.set_preferred_scale_transform(scale, transform);
+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;
+115 -74
View File
@@ -1,73 +1,30 @@
#[macro_use]
extern crate tracing;
mod animation;
mod backend;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod frame_clock;
mod handlers;
mod input;
mod layout;
mod niri;
mod render_helpers;
mod screenshot_ui;
mod utils;
mod watcher;
#[cfg(not(feature = "xdp-gnome-screencast"))]
mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
mod pw_utils;
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
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.
@@ -114,28 +71,83 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
Config::load(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) {
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.
@@ -146,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;
@@ -156,6 +169,12 @@ 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();
@@ -178,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
@@ -200,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())
@@ -209,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
@@ -235,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)
}
+716 -504
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)
}
}
}
@@ -1,78 +1,12 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{Bind, ExportMem, ImportAll, Offscreen, Renderer, Texture};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ 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 + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
+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()
}
}
+13 -6
View File
@@ -19,7 +19,7 @@ use smithay::output::{Output, WeakOutput};
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::PrimaryGpuTextureRenderElement;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
const BORDER: i32 = 2;
@@ -41,6 +41,8 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
transform: Transform,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
buffers: [SolidColorBuffer; 8],
@@ -93,6 +95,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -103,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,
);
@@ -126,6 +130,8 @@ impl ScreenshotUi {
let locations = [Default::default(); 8];
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -330,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
}
+16
View File
@@ -11,6 +11,7 @@ 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};
@@ -20,6 +21,14 @@ pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
}
pub fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
pub fn get_monotonic_time() -> Duration {
let ts = clock_gettime(ClockId::Monotonic);
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
@@ -190,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);
}
}
}