Compare commits

...

433 Commits

Author SHA1 Message Date
Ivan Molodetskikh 6a80078259 README: Bring back NVIDIA issues note 2024-04-20 17:45:17 +04:00
Ivan Molodetskikh 303c51ee20 README: Update demo video 2024-04-20 17:30:35 +04:00
Ivan Molodetskikh 37a836f462 Bump version to 0.1.5 2024-04-20 16:55:39 +04:00
Ivan Molodetskikh 361ede4bcd wiki: Mention border background window rule in the FAQ 2024-04-20 16:52:51 +04:00
Ivan Molodetskikh 4fc80124ad Move info from README to Getting Started wiki page 2024-04-20 11:24:33 +04:00
Ivan Molodetskikh ba44aeda4a wiki: Add a FAQ page 2024-04-20 10:24:20 +04:00
sodiboo b5f7e4bd83 niri_ipc::Socket; niri msg version; version checking on IPC (#278)
* Implement version checking in IPC

implement version checking; streamed IPC

streamed IPC will allow multiple requests per connection

add nonsense request

change inline struct to json macro

only check version if request actually fails

fix usage of inspect_err (MSRV 1.72.0; stabilized 1.76.0)

"nonsense request" -> "return error"

oneshot connections

* Change some things around

* Unqualify niri_ipc::Transform

---------

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

A similar change to the column movement anim is more detrimental than
it's worth.
2024-04-18 00:30:12 +04:00
Ivan Molodetskikh 3b37f1a557 Sync expel animations 2024-04-17 18:03:17 +04:00
Ivan Molodetskikh dee0abb713 wiki: Clarify animations 2024-04-17 15:10:42 +04:00
Ivan Molodetskikh bbb4a64126 Use correct animation config for tile removal 2024-04-17 14:38:34 +04:00
Ivan Molodetskikh dfe49aa705 Use movement anim for view anim during movement 2024-04-17 14:29:22 +04:00
Ivan Molodetskikh 7ca39baf9e Add view anim functions with config argument 2024-04-17 14:23:47 +04:00
Ivan Molodetskikh 73e9ef5fe2 Resolve animation defaults during parsing 2024-04-17 14:06:32 +04:00
Ivan Molodetskikh c40d4f3268 Include resized window in left move 2024-04-17 10:35:46 +04:00
Ivan Molodetskikh 1b496ee21f Clamp animated window size 2024-04-16 17:44:06 +04:00
Ivan Molodetskikh bde46dab52 wiki: Mention consume/expel in window-movement anims 2024-04-16 11:02:21 +04:00
Ivan Molodetskikh 21ef5aded8 Remove jumps on consume/expel animation start 2024-04-16 10:48:54 +04:00
Ivan Molodetskikh b288102866 Implement consume/expel animations 2024-04-16 09:58:39 +04:00
Ivan Molodetskikh ff42f9b9d3 Start move animations from add/remove window/column 2024-04-16 08:59:15 +04:00
Ivan Molodetskikh c163e58167 Animate movement and resize on window closing in a column 2024-04-16 08:16:34 +04:00
Ivan Molodetskikh a9094b43d4 wiki: Mention niri msg outputs for VRR 2024-04-16 07:56:32 +04:00
Ivan Molodetskikh 9e33320b11 wiki: Clarify window-movement animation 2024-04-15 23:07:14 +04:00
Ivan Molodetskikh c40de5364d Add vrr_supported/enabled to output IPC 2024-04-15 22:29:25 +04:00
Ivan Molodetskikh 69f723d68a Implement vertical window move animations 2024-04-15 21:19:09 +04:00
Ivan Molodetskikh 568fbe26fe Avoid continuous redrawing during horizontal gesture 2024-04-14 14:29:41 +04:00
Ivan Molodetskikh f8412ecff3 wiki: Add since to VRR 2024-04-14 13:15:52 +04:00
Ivan Molodetskikh 3c6d8062c5 Add variable-refresh-rate flag 2024-04-14 09:37:42 +04:00
Ivan Molodetskikh 40374942db tty: Shorten non-desktop check 2024-04-14 08:08:09 +04:00
Ivan Molodetskikh 2c873044e8 Restore view offset upon unfullscreening 2024-04-13 20:07:37 +04:00
Ivan Molodetskikh 1336a581a6 tile: Fix returned snapshot size 2024-04-13 18:05:56 +04:00
Ivan Molodetskikh 8b0dc1902c Set window-resize animation config for view-offset anim caused by resize 2024-04-13 14:57:55 +04:00
Ivan Molodetskikh 9d5f1c7ef7 Unify Animation- and RenderSnapshot 2024-04-13 14:16:07 +04:00
Ivan Molodetskikh 71be19b234 Implement window resize animations 2024-04-13 11:07:23 +04:00
Ivan Molodetskikh 4fd9300bdb Fix typo 2024-04-13 10:58:32 +04:00
Ivan Molodetskikh 2bb6dd8c48 Move unmapped check to a pre-commit hook 2024-04-13 09:12:32 +04:00
Ivan Molodetskikh 7319f37f7a Add render_to_encompassing_texture() 2024-04-12 20:38:51 +04:00
Ivan Molodetskikh 0cd149c939 animation: Tweak clamped duration logic 2024-04-10 12:09:54 +04:00
Ivan Molodetskikh 5383a0591f Use clamped animations where it makes sense 2024-04-10 11:28:49 +04:00
Ivan Molodetskikh 0c68609063 animation: Implement clamped value and duration 2024-04-10 11:28:02 +04:00
Ivan Molodetskikh 6cd3f96a10 Fix building on stable 2024-04-10 09:26:56 +04:00
Ivan Molodetskikh 1888696567 Reimplement window closing anim in an efficient way
- Keep a root surface cache to be accessible in surface destroyed()
- Only snapshot during / right before closing, rather than every frame
- Store textures rather than elements to handle scale and alpha properly
2024-04-10 09:14:04 +04:00
Ivan Molodetskikh b9e789619f wiki: Fix wrong since annotation spot 2024-04-09 23:56:40 +04:00
Ivan Molodetskikh dd011f1012 Implement window closing animations 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 301a2c0661 layout: Fix view jumps when removing a window on the left 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 956bf7c0a8 Add missing mouse warp to commit unmap 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 209492e700 Add ease-out-quad curve 2024-04-09 23:42:01 +04:00
Ivan Molodetskikh 7e0d3d31f7 Update Smithay 2024-04-09 19:06:13 +04:00
Ivan Molodetskikh e448cfb0ef Adjust view offset anim together with offset
Not doing this caused quickly moving a column right and left to base the
final view position on an incorrect view offset.
2024-04-08 22:16:35 +04:00
Ivan Molodetskikh 6aceb3a798 Render active column in front
Rather than just the active window. This is visible on the new window
movement animations.
2024-04-08 19:48:52 +04:00
Ivan Molodetskikh 4856522a7a Implement window open shift in terms of window-movement
This removes the quite unobvious visual size, and fixes jerking when
opening multiple windows in quick succession.
2024-04-08 19:25:45 +04:00
Ivan Molodetskikh c1432bfa96 Implement column movement animation 2024-04-08 19:11:25 +04:00
Ivan Molodetskikh ec0531264e Avoid move_left() in expel-left 2024-04-08 19:11:25 +04:00
Ivan Molodetskikh 03fc439150 layout: Fix view_offset value when moving column 2024-04-08 17:34:39 +04:00
Ivan Molodetskikh 83aec41df3 Hide pointer on touch interaction 2024-04-06 10:57:12 -07:00
Ivan Molodetskikh 8be9381974 wiki: Add gamescope to the Xwayland page 2024-04-03 17:21:36 +04:00
Ivan Molodetskikh dc56f9885c wiki: Improve Xwayland page 2024-04-03 17:16:53 +04:00
Ivan Molodetskikh 2b3a80b477 wiki: Document IPC backwards compatibility 2024-04-02 09:08:36 +04:00
Ivan Molodetskikh 294f16f76c Fix typo in comment 2024-04-02 08:44:08 +04:00
Ivan Molodetskikh 4f56ff16f9 Fix and add missing calls to DRM leasing 2024-04-01 08:30:27 +04:00
Ivan Molodetskikh fe79a6a4e2 Clarify PipeWire error message 2024-03-31 11:36:04 +04:00
Ivan Molodetskikh 950fcf6328 Set SIGPIPE to SIG_DFL before printing in niri msg 2024-03-31 09:10:15 +04:00
Ivan Molodetskikh 7ff2de19b9 wiki: Update block-out-from-screencast img 2024-03-30 13:25:54 +04:00
Ivan Molodetskikh f81b51f4c0 Bump version to 0.1.4 2024-03-30 11:39:12 +04:00
Ivan Molodetskikh a90221d924 Fix crash when stopping screencast session twice 2024-03-30 10:50:02 +04:00
Ivan Molodetskikh ab22816521 wiki: Fix default config link 2024-03-29 14:23:28 +04:00
Ivan Molodetskikh 56a55f1ad1 Improve README 2024-03-29 14:20:51 +04:00
Ivan Molodetskikh f7fde74a8d input: Add Tracy span to notify activity 2024-03-29 14:13:08 +04:00
Ivan Molodetskikh 0470a833a1 Move IPC into wiki 2024-03-29 14:13:01 +04:00
Ivan Molodetskikh 092420ec5a tty: Try to proceed when can't get render node
This is a workaround that should make split display/render devices work.
2024-03-29 09:09:33 +04:00
Ivan Molodetskikh f46e937949 wiki: Improvements 2024-03-28 21:07:48 +04:00
Ivan Molodetskikh c9a47f8283 wiki: Mention creating screenshot directory 2024-03-28 21:01:26 +04:00
Ivan Molodetskikh 9b7ed57d37 Create screenshot directory if it doesn't exist 2024-03-28 20:59:42 +04:00
Ivan Molodetskikh cf409a4ea6 wiki: Link all sections from the overview 2024-03-28 20:53:26 +04:00
Ivan Molodetskikh 83bd2317ee wiki: Add miscellaneous 2024-03-28 20:53:15 +04:00
Ivan Molodetskikh 0f19003611 default-config: Link layout wiki 2024-03-28 20:48:27 +04:00
Ivan Molodetskikh 470d65a060 wiki: Improve Layout 2024-03-28 17:50:34 +04:00
Ivan Molodetskikh 4f421907cd wiki: Add Layout 2024-03-28 17:35:36 +04:00
Ivan Molodetskikh b4eaaed19e Upgrade dependencies 2024-03-28 17:35:27 +04:00
Ivan Molodetskikh d3d178fac7 wiki: Mention niri msg focused-window 2024-03-28 13:47:27 +04:00
Ivan Molodetskikh 3091102365 Implement niri msg focused-window 2024-03-28 13:45:24 +04:00
Ivan Molodetskikh a7b3819214 tty: Add check for zero gamma size 2024-03-28 07:47:57 +04:00
Ivan Molodetskikh 1eff5aeb75 wiki: Add one more bind example 2024-03-27 21:48:19 +04:00
Ivan Molodetskikh 9f0566b1ab wiki: Fix em-dash 2024-03-27 21:47:08 +04:00
Ivan Molodetskikh 3c75082df2 wiki: Add key bindings 2024-03-27 21:46:11 +04:00
Ivan Molodetskikh 9927c15f68 Replace config transform with ipc 2024-03-27 17:03:17 +04:00
Ivan Molodetskikh cf87a185a9 Add logical output info and preferred modes to IPC 2024-03-27 14:54:24 +04:00
Ivan Molodetskikh e276c906bf Expose more info in DisplayConfig impl
Needed for the new xdp-gnome.
2024-03-27 09:46:18 +04:00
Ivan Molodetskikh 571768af43 Make ipc_outputs Arc Mutex 2024-03-27 08:27:14 +04:00
Ivan Molodetskikh c09d5eb048 wiki: Clarify refresh rate in outputs 2024-03-26 21:29:47 +04:00
Ivan Molodetskikh 1a3e31a5cf wiki: Fix order in outputs 2024-03-26 21:28:09 +04:00
Ivan Molodetskikh 62f14d42dc wiki: Add outputs section 2024-03-26 21:26:43 +04:00
Ivan Molodetskikh ce644852d2 wiki: Fix wording in Animations 2024-03-26 21:02:07 +04:00
Ivan Molodetskikh ffe9a03b58 wiki: Remove anchored link from debug page
The github wiki action rewriter can't deal with them.
2024-03-26 20:01:37 +04:00
Ivan Molodetskikh 3c84de5215 wiki: Document debug options 2024-03-26 19:53:58 +04:00
Ivan Molodetskikh cd555bbad7 wiki: Clarify config breaking change between releases 2024-03-26 19:40:23 +04:00
Ivan Molodetskikh 287d9b6b3f wiki: Clarify breaking change policy 2024-03-26 18:47:16 +04:00
Ivan Molodetskikh 9bd812c37a wiki: Add config breaking change policy 2024-03-26 18:44:42 +04:00
Ivan Molodetskikh 0845eef326 wiki: Add easing example 2024-03-26 18:35:48 +04:00
Ivan Molodetskikh 4d8cb3a6e3 wiki: Add animations page 2024-03-26 18:33:53 +04:00
Ivan Molodetskikh 48b009ba63 CI: Depend on a later commit of the github wiki action 2024-03-26 17:32:06 +04:00
Ivan Molodetskikh addd1f5267 wiki: Replace links with relative 2024-03-26 17:24:17 +04:00
Ivan Molodetskikh b30f8fb2cc wiki: Use relative links in sidebar 2024-03-26 17:21:40 +04:00
Ivan Molodetskikh f5c97faf4a wiki: Improve window rules formatting 2024-03-26 13:35:37 +04:00
Ivan Molodetskikh 8f1bbea863 wiki: Expand fullscreen rule example 2024-03-26 13:25:01 +04:00
Ivan Molodetskikh 5e7eafb2fd wiki: Add is-focused example 2024-03-26 13:21:20 +04:00
Ivan Molodetskikh 41b13aa881 wiki: Expand window rules 2024-03-26 13:18:30 +04:00
Ivan Molodetskikh fd7f2287f0 wiki: Clarify Overview 2024-03-26 13:18:18 +04:00
Ivan Molodetskikh 1635337504 wiki: Clarify screenshot UI blocking out 2024-03-26 11:25:09 +04:00
Ivan Molodetskikh b677592f11 wiki: Mention inability to unset rule 2024-03-26 11:17:40 +04:00
Ivan Molodetskikh ad2795bb27 wiki: Clarify window rule matching example 2024-03-26 11:11:26 +04:00
Ivan Molodetskikh 7826003a81 wiki: Sets->Set for consistency 2024-03-26 11:05:47 +04:00
Ivan Molodetskikh 768fbea14d CI: Download LFS files in publish-wiki 2024-03-26 10:55:10 +04:00
Ivan Molodetskikh e46003f91f default-config: Delete some input and window rule settings
Replace them with links to the wiki.
2024-03-26 10:49:49 +04:00
Ivan Molodetskikh 5360ddb320 wiki: Fix missing backtick 2024-03-26 10:44:23 +04:00
Ivan Molodetskikh d4b271fead wiki: Document window rules 2024-03-26 10:40:19 +04:00
Ivan Molodetskikh de6685f3ab wiki: Add missing commas 2024-03-26 08:21:58 +04:00
Ivan Molodetskikh 662e2df0e1 wiki: Clarify auto back and forth like in flake docs 2024-03-26 08:19:18 +04:00
Ivan Molodetskikh 26c4824047 wiki: Input and more Overview 2024-03-26 08:12:55 +04:00
Ivan Molodetskikh 78dbb2308e wiki: Start writing input configuration 2024-03-25 22:51:21 +04:00
Ivan Molodetskikh 1dce99352e wiki: Fix links 2024-03-25 21:56:37 +04:00
Ivan Molodetskikh 0b6d62f65e wiki: Add configuration overview 2024-03-25 21:50:21 +04:00
Ivan Molodetskikh cf54f75113 Move wiki into the main repository 2024-03-25 21:16:03 +04:00
Ivan Molodetskikh 0d90876ad8 CI: Disable checkout progress from nix 2024-03-25 21:13:23 +04:00
Ivan Molodetskikh e5bd1113ba default-config: Make example use screen-capture blocking 2024-03-24 11:42:27 +04:00
Ivan Molodetskikh 6f765db44e default-config: Clarify interactivity in block-out-from "screen-capture" 2024-03-24 11:37:07 +04:00
Ivan Molodetskikh 5f23d344d5 Make screenshot UI render target-aware 2024-03-24 11:25:48 +04:00
Ivan Molodetskikh e43e10f44e Remove unnecessary reference 2024-03-24 11:11:00 +04:00
Ivan Molodetskikh 493c8dc890 Implement block-out-from window rule, fix alpha on window screenshots 2024-03-24 10:22:56 +04:00
Ivan Molodetskikh 8b4a9d68e0 Implement opacity window rule 2024-03-24 08:30:26 +04:00
Ivan Molodetskikh a16a0f0e52 Implement TouchpadScroll binds 2024-03-23 20:30:45 +04:00
Ivan Molodetskikh 6ba195211b Rename WheelTracker to ScrollTracker 2024-03-23 20:17:01 +04:00
Ivan Molodetskikh afaaf36f27 Avoid scroll bind lookup until it is triggered 2024-03-23 19:20:44 +04:00
Ivan Molodetskikh f1b36b0dce Send pending configure after recomputing window rules 2024-03-23 18:57:06 +04:00
Ivan Molodetskikh 6ec65bc0d6 Add is-focused window rule matcher 2024-03-23 16:16:52 +04:00
Ivan Molodetskikh d65446421f Make rules non-pub 2024-03-23 16:04:35 +04:00
Ivan Molodetskikh 24078cfea2 Make need_to_recompute_rules non-pub 2024-03-23 16:02:23 +04:00
Ivan Molodetskikh 5cc2c31a5b Split State::refresh() to get a trace span 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh b7ed2fb82a Add is-active window rule matcher 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh f3f02aca20 Lift output clones from queue_redraw() 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh 021a2a1af7 Don't use an idle for queued redraw tracking
This way we can order the redraw after all the refreshing, where it
should be.
2024-03-23 15:45:44 +04:00
Ivan Molodetskikh 354f0b039a Pass Un/Mapped to window rule resolution 2024-03-23 15:45:44 +04:00
Andreas Stührk d120e0c451 input: Add support for ISO level3 shift modifier
This modifier is typically called "AltGr" on keyboards or "Mod5" in xkb
layouts. Requires a Smithay update.
2024-03-23 15:45:27 +04:00
Ivan Molodetskikh 0f724f2011 Stop hardcoding "us" default layout
XKB has its own way to pick the default.
2024-03-23 10:10:01 +04:00
Ivan Molodetskikh 46131c87a5 default-config: Clarify that wheel binds are affected by natural-scroll 2024-03-23 09:02:50 +04:00
Ivan Molodetskikh c66319314e Fix vertical wheel binds on winit 2024-03-23 09:00:55 +04:00
Ivan Molodetskikh b09dbb80c7 [cfg-breaking] Rename Wheel* to WheelScroll* bindings
Less confusion, and clearer that they are affected by natural-scroll.
2024-03-23 08:49:58 +04:00
Ivan Molodetskikh 54e6a01284 Allow clippy false positive harder 2024-03-22 21:24:11 +04:00
Ivan Molodetskikh 7721e3fc44 Allow clippy false positive 2024-03-22 21:14:03 +04:00
Ivan Molodetskikh 0d2fdb49ef default-config: Add mouse wheel binds 2024-03-22 20:56:20 +04:00
Ivan Molodetskikh b06e51da60 Implement bind cooldown-ms 2024-03-22 20:47:40 +04:00
Ivan Molodetskikh 6c08ba307a input: Make functions return the whole bind 2024-03-22 20:47:35 +04:00
Ivan Molodetskikh 4b2fdd0776 Implement mouse wheel bindings 2024-03-22 13:10:40 +04:00
Ivan Molodetskikh 969519b5d8 input: Generalize bound_action() to Trigger 2024-03-22 11:11:45 +04:00
Ivan Molodetskikh a0c8c39b06 Make binds accept wheel names 2024-03-22 10:36:19 +04:00
Ivan Molodetskikh 977f1487c2 input: Fix discrete axis value on winit 2024-03-22 09:41:10 +04:00
Ivan Molodetskikh fbe021fbdf input: Rename discrete => v120 2024-03-22 09:35:17 +04:00
Ivan Molodetskikh db49deb7fd Implement draw-border-with-background window rule 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh c61361de3c Implement window rule reloading and min/max size rules 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh 3963f537a4 Wrap mapped windows in a Mapped 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh f31e105043 Make window a subdirectory 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh bbb4caeb8c Remove remaining Window-specific functions 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh d421e1fbf8 Move PartialEq from LayoutElement to an associated type 2024-03-19 18:29:13 +04:00
FluxTape 23ac3d7323 Workspace back and forth (#253)
* implement workspace back and forth

* Make our own ID counter instead of SerialCounter, use a newtype

* Rename FocusWorkspaceBackAndForth to FocusWorkspacePrevious

* Add focus-workspace-previous to tests

* Don't special case in switch_workspace_previous

* Minor clean up

* Add switch_workspace_auto_back_and_forth to tests

* Skip animation on switch_workspace_previous

* Preserve previous_workspace_id on workspace movement

* Make Workspace::id private with a getter

Reduce the chance it gets overwritten.

* Add test for workspace ID uniqueness

* Update previous workspace ID upon moving workspace across monitors

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-19 07:27:52 -07:00
Ivan Molodetskikh c3327d36da tty: Generalize DRM property helpers 2024-03-19 09:00:00 +04:00
Ivan Molodetskikh e0da101c73 Disable screencast when PipeWire is missing
This can cause a panic.
2024-03-19 08:59:28 +04:00
Ivan Molodetskikh 4740682904 README: Move configuration up 2024-03-18 19:36:18 +04:00
Ivan Molodetskikh df9d721f74 Implement focus-follows-mouse 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh d970abead8 Keep track of output and window in PointerFocus separately 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh 4f6ed9dfc9 Fix lock surface pointer location 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh 84302796dc Take workspace switch gesture into account for visual rect 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a39e703fc3 Don't warp if currently using tablet
The tablet will override the position anyway.
2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a55db6c6c4 Warp mouse to focus on window closing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a011b385d8 Warp mouse to focus on new window appearing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 2984722f80 Warp mouse only if layout is focused 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 118773e17d Track keyboard focus component 2024-03-18 19:31:11 +04:00
FluxTape 741bee461c Implement warp-mouse-to-focus 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 0c57815fbf Restore gamma on TTY switch back 2024-03-15 22:02:29 +04:00
Ivan Molodetskikh cf89c789c3 README: Link touchpad gestures to showcase video 2024-03-15 09:56:11 -07:00
Ivan Molodetskikh 642c6e7512 Store gamma changes to apply on session resume 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh 6839a118bb Implement gamma adjustment via GAMMA_LUT property 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh 9ae3cad82b gamma-control: Misc. clean ups and fixes 2024-03-15 13:29:36 +04:00
phuhl 89dfaa6cac Adds support for wlr_gamma_control_unstable_v1 protocol 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh f6ffe8b3ab tty: Make binding EGL wl-display optional 2024-03-14 18:08:52 +04:00
la .uetcis cc83ff008d Add clickfinger in touchpad config (#256)
* Add clickfinger in touchpad config

* Change `clickfinger` to `click-method`

* Change `bottom_areas` to `button_areas`

* Change button_areas to button-areas

For consistency.

* Reorder click methods in error message

The most usual one comes first.

* default-config: Move click-method down

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-13 21:26:03 -07:00
Ivan Molodetskikh ba4e7481c3 default-config: Clarify how to power on monitors 2024-03-14 08:04:34 +04:00
Ivan Molodetskikh c15bc2a028 tty: Set max bpc to 8 2024-03-13 09:15:18 +04:00
Ivan Molodetskikh bf1cc98886 Update Smithay 2024-03-13 07:17:19 +04:00
Ivan Molodetskikh 5f137b77d3 Reapply "Add wp-viewporter"
This reverts commit 40cec34aa4.

The Chromium issues are now fixed.
2024-03-12 17:22:53 +04:00
Ivan Molodetskikh 128d573e74 Update Smithay (viewporter fixes) 2024-03-12 17:22:52 +04:00
Ivan Molodetskikh ed8a6afe80 Add a 1 Hz fallback frame callback timer
gamescope + Minecraft with NeoForge throws an error upon starting if
there are no frame callbacks, thus making it the first client that has a
problem. Also, apparently, Veloren disconnects from server with VSync
and no frame callbacks.
2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 43aa2f95be Fix new clone_from Clippy lints 2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 5c0a1f4d6f Fix spelling mistake 2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 8c46611c29 Preserve view offset for activate_prev_column_on_removal 2024-03-10 17:59:10 +04:00
Ivan Molodetskikh 40cec34aa4 Revert "Add wp-viewporter"
This reverts commit 348690afb6.

Apparently this breaks input in Chromium: the input region won't resize
together with the window.
2024-03-10 17:59:10 +04:00
Ivan Molodetskikh 1971a41fdd utils/spawning: Pass grandchild PID only on systemd
libc::close_range() is not available on musl, so do this workaround for
now.
2024-03-09 18:37:11 +04:00
Ivan Molodetskikh 4ea90140d4 Fix warning on --no-default-features 2024-03-09 18:36:01 +04:00
Ivan Molodetskikh acd33653b3 README: Update screenshot 2024-03-09 14:45:18 +04:00
Ivan Molodetskikh f7c6516da7 README: Expand package listing 2024-03-09 14:29:20 +04:00
Ivan Molodetskikh b220420fba README: mention just "Touchpad gestures"
We've got both directions now.
2024-03-09 08:29:51 +04:00
Ivan Molodetskikh bbeaba16a0 Bump version to 0.1.3 2024-03-09 08:28:48 +04:00
Ivan Molodetskikh 9d7c39b89a Reposition outputs after potentially changing mode
Currently outputs aren't repositioned again after a mode change, which
can cause overlaps.
2024-03-09 08:23:57 +04:00
Ivan Molodetskikh 03fe864d07 Add xdg-foreign 2024-03-08 17:08:58 +04:00
Ivan Molodetskikh e45dbb8ef6 Pass through subpixel layout 2024-03-08 17:06:46 +04:00
Ivan Molodetskikh 5c4b71a5a4 Update Smithay and dependencies 2024-03-08 17:06:35 +04:00
Ivan Molodetskikh 348690afb6 Add wp-viewporter
Doesn't hurt I guess.
2024-03-08 16:52:54 +04:00
sodiboo ca22e70cc4 Implement wlr-screencopy v1 (#243)
* Implement wlr-screencopy

* Finish the implementation

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

Also, it meant that y_invert is no longer needed.

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

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

---------

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

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

* Make systemd and dinit environment activation additive

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

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

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

* parse only one default-column-width

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

* use proper filename for config errors if possible

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

- niri queues frame
- successful VBlank happens, sequence is bumped, frame callbacks are
  sent
- niri receives commit, redraws, queues next frame, tries to send frame
  callbacks, but there wasn't a new VBlank yet, so the sequence is old,
  and frame callbacks aren't sent
- frame callbacks are sent only next VBlank
2024-03-01 12:56:55 +04:00
Ivan Molodetskikh 0add457cf0 tty: Avoid zero estimated vblank timer 2024-03-01 08:27:44 +04:00
Ivan Molodetskikh 6e5426ef22 Fix center-column regression
Mistake introduced along with the horizontal gesture.
2024-03-01 08:09:03 +04:00
Ivan Molodetskikh 202406aadf Fix presentation feedback panic with zero presentation time 2024-03-01 07:55:09 +04:00
Ivan Molodetskikh 92d9c7ff4f Add emulate-zero-presentation-time debug flag 2024-03-01 07:54:58 +04:00
Ivan Molodetskikh 28977d1d3f Move workspace gesture into monitor & fix missing workspace cleanup 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh ba10bab010 Implement horizontal touchpad swipe 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh 55038b7c07 Pass prev_idx explicitly to animate_view_offset_to_column() 2024-02-29 08:30:46 +04:00
Ivan Molodetskikh 8018839f5d Extract animate_view_offset_to_column() 2024-02-28 17:23:03 +04:00
Ivan Molodetskikh 077f22edd6 Append _fit to animate_view_offset_to_column() 2024-02-28 17:21:08 +04:00
Ivan Molodetskikh 4f7c3300ef Upgrade dependencies 2024-02-28 13:45:12 +04:00
Ivan Molodetskikh 5628bf7d77 Update Smithay 2024-02-28 13:23:15 +04:00
Christian Meissl 719697179f input: add basic touch support 2024-02-28 13:19:41 +04:00
Christian Meissl 5ac350d51c chore: update smithay 2024-02-28 13:19:41 +04:00
Ivan Molodetskikh 494e98c123 Parse CSS colors in {in,}active-color 2024-02-26 09:14:35 +04:00
Ivan Molodetskikh ec156a8587 Add environment {} config section 2024-02-24 10:08:56 +04:00
Ivan Molodetskikh e278e871c3 Expand ~ in spawn 2024-02-24 09:16:44 +04:00
Ivan Molodetskikh ab9d1aab4e Add open-fullscreen window rule 2024-02-24 08:44:21 +04:00
Ivan Molodetskikh 506dcd99d7 Handle un-/fullscreen after initial configure 2024-02-23 17:47:12 +04:00
Ivan Molodetskikh dfbc024127 Rename surface -> toplevel 2024-02-23 17:40:30 +04:00
Ivan Molodetskikh eb2dce1b53 Fix default width fixed not being honored with borders 2024-02-23 14:40:56 +04:00
Ivan Molodetskikh f5b776a947 Fix unset default width causing a window resize right away 2024-02-23 14:31:35 +04:00
Ivan Molodetskikh 6a587245eb Add open-maximized window rule 2024-02-23 14:24:39 +04:00
Ivan Molodetskikh 2317021a7c Implement explicit unmapped window state tracking 2024-02-23 14:01:32 +04:00
Ivan Molodetskikh af6485cd8c Fix new warnings 2024-02-22 14:04:18 +04:00
Ivan Molodetskikh f32a25eefe Improve shader formatting 2024-02-22 10:21:38 +04:00
Ivan Molodetskikh aefbad0cf7 Simplify gradient border shader 2024-02-22 10:17:06 +04:00
Ivan Molodetskikh b091202d86 visual-tests: Add gradient angle and area tests 2024-02-22 08:54:35 +04:00
Ivan Molodetskikh 48f0f6fb3c Implement gradient borders 2024-02-21 22:15:21 +04:00
Ivan Molodetskikh 340bac0690 Remove unnecessary crop bounds during workspace switch 2024-02-21 21:41:12 +04:00
Ivan Molodetskikh d1b8134337 focus-ring: Store config instead of individual fields 2024-02-21 20:54:24 +04:00
Ivan Molodetskikh 646e3d8995 Accept location in FocusRing
Makes it work more like other elements.
2024-02-21 11:08:48 +04:00
Ivan Molodetskikh d1fe6930a7 Move UI elements into submodule 2024-02-21 10:50:30 +04:00
Ivan Molodetskikh 9e60b344d0 Move watcher to utils 2024-02-21 10:45:03 +04:00
Ivan Molodetskikh 2c01cde9be Move spawn to submodule 2024-02-21 10:42:21 +04:00
Ivan Molodetskikh cb9dc9c0cd Move utils to subfolder 2024-02-21 10:33:09 +04:00
Ivan Molodetskikh 73d2807b4b Fix move_window_to_output losing window instead 2024-02-21 09:39:32 +04:00
Ivan Molodetskikh 7d41f113cb Change non-bug error! to warn!
Be consistent with our usage.
2024-02-21 09:20:34 +04:00
Ivan Molodetskikh 63e5cf8798 Add missing qualified path 2024-02-21 09:12:42 +04:00
Ivan Molodetskikh 9ce19ad7de Use niri_render_elements! for the screenshot UI 2024-02-21 09:12:40 +04:00
Ivan Molodetskikh 751f79dc35 Comment out toggle-debug-tint default bind 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh b8aa0a86e7 Fix debug tint desync for new outputs 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh 82fffdea80 Fix locking with DPMS-inactive monitors
This both enables locking while monitors are powered off (they have no
buffer attached at that point on a TTY, so no sensitive content can
become visible), and fixes the condition below to check even if the
rendering was skipped.
2024-02-21 07:40:50 +04:00
Ivan Molodetskikh 5b3bfd95d9 Upgrade logs about removing env vars to warn!
These are more visible now with the --session flag.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 1a15aa704d ci: Check individual features 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh d58a45a96c Add systemd feature flag for systemd-specific things 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 9f1b4ee299 Set XDG_CURRENT_DESKTOP and XDG_SESSION_TYPE from niri itself 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh f0a5e9c933 Add --session CLI flag instead of detection based on systemd service
Allows running without systemd.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh c4c07841d7 niri.service: Put into session.slice
Now that we're separating spawned processes, put ourselves in the more
important session.slice.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 6ba24e341f utils/spawn: Put processes into systemd scopes
This separates them from the niri scope for the purposes of e.g. the OOM
killer only killing the app and not the compositor.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 13b6c74cc3 utils/spawn: Receive grandchild PID 2024-02-20 12:49:52 +04:00
Ivan Molodetskikh d8fb8d5ef0 Update for Smithay MultiGpu shadow copies 2024-02-18 21:12:07 +04:00
Ivan Molodetskikh 2b5eeb6162 Fix fullscreen handling before initial configure 2024-02-18 10:20:34 +04:00
Ivan Molodetskikh 85be5f746c default-config: Clarify how indexed workspace access works 2024-02-17 21:01:10 +04:00
Ivan Molodetskikh dd7362913e Ignore mouse releases for dismissing overlays 2024-02-17 14:07:51 +04:00
Ivan Molodetskikh 62892d6361 Prevent locking while another lock client is already active
Fixes double swaylock from manual + swayidle.
2024-02-17 07:47:06 +04:00
Ivan Molodetskikh 31c13b6a69 default-config: Document enable-color-transformations-capability debug flag 2024-02-17 07:23:43 +04:00
Ivan Molodetskikh baaac2f3c4 Update Smithay 2024-02-16 22:40:37 +04:00
Ivan Molodetskikh 3fdefae45b Bump version to 0.1.2 2024-02-16 18:00:19 +04:00
Ivan Molodetskikh 6345224e95 default-config: Fix spelling mistakes
Ok I added automatic :set spell for KDL now.
2024-02-16 17:40:18 +04:00
Ivan Molodetskikh b3d2096439 Replace set_modified() with manual impl
MSRV moment
2024-02-16 08:46:58 +04:00
Ivan Molodetskikh 94ded2f6a9 CI: Add a MSRV job 2024-02-16 08:33:19 +04:00
Ivan Molodetskikh fa3bc69f94 Add watcher tests 2024-02-15 10:31:53 +04:00
Viktor Pocedulic 363e1d8764 input: enable configuring of trackpoint devices 2024-02-15 10:27:12 +04:00
Ivan Molodetskikh 8e1d4de0dc tty: Filter out interlaced modes
They don't seem to work. wlroots also filters them:
https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/feb54979c0940655e36119c63e18a9ee72cc03b0/backend/drm/drm.c#L1461
2024-02-14 21:14:01 +04:00
Ivan Molodetskikh 72e3fadb9a default-config: Specify example refresh rate with 3 digits
This is the format you need to use.
2024-02-14 19:55:31 +04:00
Ivan Molodetskikh 78cda2e67f tty: Truncate Edid strings to nul
Otherwise they crash in wayland-rs when converting to CString.
2024-02-14 19:49:34 +04:00
Ivan Molodetskikh 924e21f69b Focus output unconditionally after moving window there
Fixes output not getting focus if there was no window to move.
2024-02-14 09:06:13 +04:00
Ivan Molodetskikh befdebfa03 Add the beginnings of window rules 2024-02-14 08:32:14 +04:00
Ivan Molodetskikh 7960a73e9d config: Fix missing layout {} defaulting to 0 gaps 2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 749ee5d627 Do initial configuration right before sending initial configure
Let the toplevel fill in some details about itself.
2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 952dd48115 Deduplicate call to miette hook 2024-02-13 12:16:58 +04:00
Ivan Molodetskikh cbd066ab68 default-config: Document animation properties 2024-02-12 20:46:29 +04:00
Ivan Molodetskikh bccde351fb Update flake.lock 2024-02-12 09:58:04 +04:00
Kiara Grouwstra beaffb1b97 CI: check nix build works 2024-02-12 09:57:34 +04:00
Shawn Wallace 385454378b Implement DRM leasing
Closes #178
2024-02-12 09:48:54 +04:00
Ivan Molodetskikh 18f06a7acd Fix border getting default values for focus ring 2024-02-12 09:34:54 +04:00
Ivan Molodetskikh 6e23073019 Move default_border() into FocusRing 2024-02-12 09:22:22 +04:00
Ivan Molodetskikh a9fcbf81eb Export NIRI_SOCKET to systemd/dbus environment 2024-02-12 08:56:39 +04:00
Ivan Molodetskikh a99f34cba8 tty: Activate monitors on session resume 2024-02-12 08:45:45 +04:00
Ivan Molodetskikh bd2277fa25 tty: Notify idle activity on session resume 2024-02-12 08:42:34 +04:00
Ivan Molodetskikh 67182129ff Add skip-confirmation flag to the quit action 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh d6b116d229 Add missing space 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh c20a843ab2 Add log message when confirming exit dialog 2024-02-12 07:53:48 +04:00
Kiara Grouwstra 1b752fe08f exclude visual tests from nix, closes #181 2024-02-12 00:01:03 +04:00
Ivan Molodetskikh 89f74aae98 freedesktop-screensaver: Filter out non-interesting messages 2024-02-11 23:05:37 +04:00
Ivan Molodetskikh 5e553c2679 Implement org.freedesktop.ScreenSaver Inhibit
xdg-desktop-portal currently has no way of disabling the Inhibit portal
or ever returning an error to the application from it. Thus Flatpak
Firefox will never fall back to its Wayland backend. To remedy this,
let's actually implement the FDO Inhibit interface that the portal can
use.
2024-02-11 22:26:59 +04:00
Ivan Molodetskikh cabf712821 hotkey-overlay: Deduplicate Spawn actions 2024-02-11 09:27:34 +04:00
Ivan Molodetskikh 0931447ec1 Implement error reporting in IPC 2024-02-11 09:19:37 +04:00
Ivan Molodetskikh a388c25795 Update dependencies 2024-02-10 15:01:34 +04:00
Ivan Molodetskikh 5c4d9824a4 Remove logind-zbus dependency
It isn't updated and we don't really need it anyway.
2024-02-10 14:58:22 +04:00
Ivan Molodetskikh ca4ee5ae25 hotkey-overlay: Only show Spawn binds with Mod/Super 2024-02-10 14:37:38 +04:00
Ivan Molodetskikh 93e16a6582 Implement niri msg action 2024-02-10 09:40:32 +04:00
Ivan Molodetskikh 3486fa5536 Remove unused directories workspace dep 2024-02-10 09:34:35 +04:00
Ivan Molodetskikh c022d74c82 Remove extra `` in comment 2024-02-10 09:19:08 +04:00
Ivan Molodetskikh e68641c0a7 Move CLI types to submodule 2024-02-10 08:40:13 +04:00
Ivan Molodetskikh 2a892ef511 input: Fix Clippy warning 2024-02-10 08:38:19 +04:00
Ivan Molodetskikh 90c6721e97 config: Add missing Smithay feature
Fixes build on nightly.
2024-02-10 07:51:53 +04:00
Ivan Molodetskikh e5cd9e9307 default-config: Replace Mod with Super in swaylock bind
Otherwise it conflicts with Mod+L in nested.
2024-02-09 16:23:33 +04:00
Ivan Molodetskikh 573dca10cc input: Fix handling of binds with compositor mod but no explicit Mod 2024-02-09 16:23:05 +04:00
Ivan Molodetskikh 577fba82e5 input: Split bound_action() and add tests 2024-02-09 16:16:18 +04:00
Ivan Molodetskikh b9116c579a Implement idle-notify and idle-inhibit 2024-02-09 15:50:40 +04:00
Ivan Molodetskikh d8dcadc5b2 Clamp animation slowdown to sane values 2024-02-07 20:03:23 +04:00
Ivan Molodetskikh 6424a2738d Make all animations configurable 2024-02-07 17:14:24 +04:00
Ivan Molodetskikh 753a90430a animation: Accept ms as u32
Less boilerplate elsewhere.
2024-02-07 16:32:38 +04:00
Ivan Molodetskikh f9085db564 Implement window open animations 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 49ce791d13 Add a Tracy span to OffscreenRenderElement::new 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 4b8e04da04 Activate the new right_of window on its workspace
This way when a dialog opens on a different workspace, the user will see
it right away when they switch to that workspace.
2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 026ad8f377 Add a way to override the element ID for primary output check 2024-02-07 11:30:52 +04:00
Ivan Molodetskikh 0761401650 Add OffscreenRenderElement 2024-02-07 11:30:33 +04:00
Ivan Molodetskikh 3360517f62 Clear before rendering to texture
Otherwise I see artifacts on some GTK dialogs.
2024-02-07 11:18:55 +04:00
Ivan Molodetskikh 9896fd67a0 Open dialogs to the right of their parent, don't steal focus 2024-02-07 10:49:01 +04:00
Ivan Molodetskikh 15ec699fbb visual-tests: Remove "Just" prefix 2024-02-07 09:24:41 +04:00
Ivan Molodetskikh a1cc39a437 visual-tests/tile: Disable focus ring 2024-02-07 09:22:00 +04:00
Ivan Molodetskikh 738d9a2b40 Add blank line 2024-02-06 19:53:31 +04:00
Ivan Molodetskikh 68752db51b layout: Add Column::advance_animations() 2024-02-06 19:52:47 +04:00
Ivan Molodetskikh d4929b8e18 Inline variable 2024-02-06 19:52:10 +04:00
Ivan Molodetskikh 93c547f749 Move focus ring into Tile
For now, will make the open animation better.
2024-02-06 19:49:51 +04:00
Ivan Molodetskikh e2b91c0c1c layout: Fix refresh in tests
Didn't affect anything but still.
2024-02-06 19:09:27 +04:00
Ivan Molodetskikh 322b5cbac7 Add Layout::with_options() 2024-02-06 19:09:15 +04:00
Ivan Molodetskikh 592791611a Change render functions to accept iterators 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh d073d2ab3d Move render functions to render_helpers 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh b2298db5c5 Split render_helpers.rs 2024-02-06 11:25:25 +04:00
Ivan Molodetskikh baa6263cbe Bump libinput to 1.21, add dwtp flag 2024-02-06 09:54:46 +04:00
Ivan Molodetskikh 795da53d53 README: Update Ubuntu dependencies 2024-02-06 09:49:53 +04:00
Ivan Molodetskikh 122afff7d1 Add niri-visual-tests 2024-02-06 09:40:45 +04:00
Ivan Molodetskikh d2a4e6a0cb Update dependencies 2024-02-06 09:40:34 +04:00
Ivan Molodetskikh 8916b18c6b Run Ubuntu CI in a 23.10 container
We will soon need newer dependencies.
2024-02-06 09:40:32 +04:00
Ivan Molodetskikh b0d0fce5f3 Move use into feature-gated function 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3dc4a5fdac Fix Clippy warnings 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 1706a46b2b layout: Mark some things as pub 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3789d85588 Add lib.rs, become a mixed lib-bin crate
Will be used for visual tests.
2024-02-05 17:40:16 +04:00
Dennis Ranke 3a23417e98 Add consume-or-expel-window-left/right commands 2024-02-05 14:09:47 +04:00
Ivan Molodetskikh 6bb83757ee Convert everything to niri_render_elements! {} 2024-02-05 14:05:08 +04:00
Ivan Molodetskikh b62a07956a Add niri_render_elements! {}
We will be using this in several other places.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 96016790b2 layout: Replace with_tiles_in_render_order() with Iterator 2024-02-05 13:55:09 +04:00
Ivan Molodetskikh bf978fe98d layout/tile: Return Iterator of render elements
Avoid a Vec.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 57521c69c3 layout: Add TileRenderElement 2024-02-04 22:52:11 +04:00
Ivan Molodetskikh da826e42aa layout: Add LayoutElementRenderElement
Allows for testing layout rendering without Wayland windows.
2024-02-04 22:31:44 +04:00
Ivan Molodetskikh b824cf90ab layout: Generalize traversal between rendering and input 2024-02-04 22:10:26 +04:00
Ivan Molodetskikh 7a4bb8ba8a layout: Make rendering not Window-specific
Doesn't need to be any more.
2024-02-04 21:23:00 +04:00
Ivan Molodetskikh 72c8f569ac Bump version to 0.1.1 2024-02-03 10:00:06 +04:00
Ivan Molodetskikh 798d9c55df Support fullscreen for new windows 2024-02-03 09:45:26 +04:00
Ivan Molodetskikh 05613eed1e Verify that pending fullscreen matches column 2024-02-03 09:44:34 +04:00
Ivan Molodetskikh b23dd4b800 Respect natural-scroll for workspace switch gesture 2024-02-03 09:00:08 +04:00
Ivan Molodetskikh 1f72089a46 Place new workspace after current when moving
This feels more natural, also makes moving back and forth idempotent in
most cases.
2024-02-03 08:42:56 +04:00
Ivan Molodetskikh fbe9020915 Update dependencies 2024-02-02 17:04:17 +04:00
Ivan Molodetskikh 2036116f16 config: Premultiply alpha in Color when converting to f32
Smithay wants premultiplied alpha.
2024-02-01 18:53:45 +04:00
Ivan Molodetskikh 9afd728ae9 Add error messages to backend initialization 2024-02-01 16:55:46 +04:00
Andreas Stührk e51268a39e Add actions to move the active workspace to another monitor 2024-02-01 12:29:46 +04:00
Ivan Molodetskikh 0a715ce155 default-config: Improve wording for focus-ring/border comment
SSD or server-side decorations is never mentioned elsewhere.
2024-02-01 12:06:13 +04:00
Ivan Molodetskikh 89ac958670 default-config: Document how focus ring and border draw behind
Related: https://github.com/YaLTeR/niri/issues/150
2024-02-01 10:08:15 +04:00
Ivan Molodetskikh 2e50f8dee0 Hardcode winit transform for now 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 7052f0129e Stop screencasts on size changes 2024-01-31 23:02:38 +04:00
axtloss 962e159db6 Add option to rotate outputs 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 11bff3a2f1 Update Smithay (rotation fix) 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 15606304f2 README: Bring AUR link back 2024-01-30 22:36:30 -08:00
Christian Meissl 85eac9d9d0 chore: bump smithay
includes fixes for wrong direct scan-out transform
and damage artifacts on output transform changes.
also includes a fix for a race in popup surface re-use.
2024-01-30 15:30:31 +04:00
Ivan Molodetskikh d3f4583c90 foreign_toplevel: Use OutputHandler to send output_enter on demand 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh fefb1cccd6 foreign_toplevel: Update the focused window last 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh deef52519a foreign_toplevel: Change activated to mean keyboard focus 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh 59ff331597 Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
2024-01-30 12:30:57 +04:00
Christian Meissl b813f99abd tty: reset surface state after changing monitor state
changing the "ACTIVE" property of a surface requires
to re-evaluate the surface state.
2024-01-30 08:03:21 +04:00
Ivan Molodetskikh d9b9cec8b8 README: Remove AUR link for now
It doesn't work properly yet apparently.
2024-01-29 12:29:32 -08:00
Christian Meissl 597ea62d17 input: update keyboard led state 2024-01-28 23:43:08 +04:00
Ivan Molodetskikh 51243a0a50 Show notification about creating a default config 2024-01-28 17:15:47 +04:00
Ivan Molodetskikh 0ebcc3e0d6 Create default config file if missing 2024-01-28 17:15:33 +04:00
Ivan Molodetskikh 64c85d865e winit: Don't remove output on CloseRequested
More winit events can process after CloseRequested, which will cause a
panic if trying to access the now-removed output.
2024-01-28 16:30:29 +04:00
Ivan Molodetskikh 367e4955ea Mark Msg as pub
Seems to break the build on 1.72.0 otherwise.
2024-01-28 09:34:42 +04:00
Ivan Molodetskikh dd967554d1 Bump version to 0.1.0 2024-01-27 14:10:31 +04:00
Ivan Molodetskikh 6d7c220137 Try harder to find an output for the screenshot UI
The mouse might be outside any outputs, let's try to open in that case
anyway.
2024-01-27 14:09:55 +04:00
Ivan Molodetskikh d77aac1afa Fix damage when rendering to texture 2024-01-27 10:50:40 +04:00
Ivan Molodetskikh 837a0a20fb Update README 2024-01-25 08:34:42 +04:00
Ivan Molodetskikh ecdf756b55 Name output render element better 2024-01-25 08:02:33 +04:00
Christian Meissl 73f3c160b2 use pixman for cursor plane rendering 2024-01-25 07:49:51 +04:00
Christian Meissl 5f99eb13ab Remove hack for fixed EGLDisplay issue 2024-01-25 07:49:51 +04:00
Christian Meissl 20326b093c Update smithay 2024-01-25 07:49:51 +04:00
Ivan Molodetskikh 467d92a4b4 github: Add a feature request link to start a discussion 2024-01-23 17:41:35 +04:00
Ivan Molodetskikh 15bb69c0b9 Update issue templates 2024-01-23 05:36:19 -08:00
Ivan Molodetskikh adfbfdffb3 Create a bug report template 2024-01-23 05:34:38 -08:00
Ivan Molodetskikh 087ed260c5 Update Smithay (find_popup_root_surface() panic fix) 2024-01-23 17:12:47 +04:00
Ivan Molodetskikh f5642ab733 Ignore popup grabs when IME keyboard grab is active
Doing this properly will require more refactors, potentially in Smithay.
For now let's just ignore popup grabs to make popups work.
2024-01-23 17:05:08 +04:00
Ivan Molodetskikh ab9706cb30 screencast: Emit MonitorsChanged 2024-01-23 12:02:52 +04:00
Ivan Molodetskikh 05f2a3709b srceencast: Send stream size
Kooha requires this (even though it's optional). Unfortunately, Kooha
also seems to want memfd recording so it doesn't work anyway.
2024-01-23 11:36:11 +04:00
Ivan Molodetskikh 743173ef64 config: Bump precision on the default widths
This seems to actually matter on my 2560x display.
2024-01-22 20:43:33 +04:00
Ivan Molodetskikh cbbb7a26fc Update Smithay, use device changed session resume code
Should fix most cases of monitors failing to light up after a TTY
switch.
2024-01-22 16:13:39 +04:00
sodiboo 18566e3366 Watch for canonical filename, not just mtime 2024-01-22 07:42:45 +04:00
Ivan Molodetskikh df48337d83 tty: Delay output config update until resume
We can't do anything while paused.
2024-01-21 10:25:39 +04:00
Ivan Molodetskikh f5e9b40140 tty: Check changes against pending connectors and mode
If we queued some DRM changes, they will be in pending. Also be more
resilient by removing unwrap.
2024-01-21 10:24:42 +04:00
Ivan Molodetskikh 5cacd03e85 Return error instead of broken screenshot for portal 2024-01-21 10:03:13 +04:00
126 changed files with 20481 additions and 3561 deletions
+1
View File
@@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text
+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)
+109 -24
View File
@@ -24,6 +24,7 @@ jobs:
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -32,34 +33,90 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.configuration }}
- name: Build (no default features)
run: cargo build ${{ matrix.release-flag }} --no-default-features
- name: Check (no default features)
run: cargo check ${{ matrix.release-flag }} --no-default-features
- name: Build
run: cargo build ${{ matrix.release-flag }}
- name: Check (just dbus)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
- name: Check (just systemd)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
- name: Check (just dinit)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
- name: Check (just xdp-gnome-screencast)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
- name: Check
run: cargo check ${{ matrix.release-flag }}
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build Tests
run: cargo test --no-run --all ${{ matrix.release-flag }}
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
visual-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --package niri-visual-tests
msrv:
strategy:
fail-fast: false
name: 'msrv - 1.72.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@1.72.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-targets
clippy:
strategy:
@@ -67,6 +124,7 @@ jobs:
name: clippy
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -75,15 +133,12 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal --component clippy
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
@@ -119,8 +174,38 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build
- run: cargo build --all
nix:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- 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
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
Generated
+870 -415
View File
File diff suppressed because it is too large Load Diff
+42 -26
View File
@@ -1,5 +1,8 @@
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.0-beta.1"
version = "0.1.5"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -7,11 +10,14 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
bitflags = "2.4.2"
directories = "5.0.1"
serde = { version = "1.0.195", features = ["derive"] }
anyhow = "1.0.81"
bitflags = "2.5.0"
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.5", default-features = false }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.0", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -35,38 +41,41 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.79" }
anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.1.1", optional = true }
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.2"
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
clap = { version = "4.4.18", features = ["derive", "string"] }
bitflags.workspace = true
bytemuck = { version = "1.15.0", features = ["derive"] }
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.7.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.27.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.152"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
logind-zbus = { version = "3.1.2", optional = true }
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
libc = "0.2.153"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.5", path = "niri-config" }
niri-ipc = { version = "0.1.5", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.18.0"
pipewire = { version = "0.7.2", optional = true }
png = "0.17.11"
pangocairo = "0.19.2"
pipewire = { version = "0.8.0", optional = true }
png = "0.17.13"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.13"
profiling = "1.0.15"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.111"
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -80,6 +89,7 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
@@ -88,15 +98,20 @@ features = [
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
xshell = "0.2.5"
[features]
default = ["dbus", "xdp-gnome-screencast"]
# Enables DBus support (required for xdp-gnome and power button inhibiting).
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables dinit integration (global environment).
dinit = []
[profile.release]
debug = "line-tables-only"
@@ -108,7 +123,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~beta.1"
version = "0.1.5"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -119,3 +134,4 @@ assets = [
]
[package.metadata.generate-rpm.requires]
alacritty = "*"
fuzzel = "*"
+21 -159
View File
@@ -6,7 +6,11 @@
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/16f87a4a-afac-49aa-b3e6-5e6f16c943a9)
<p align="center">
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/2b246c2c-7cf3-4a11-96eb-ad0c7f2f4ed6)
## About
@@ -16,26 +20,35 @@ Opening a new window never causes existing windows to resize.
Every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
Since windows go left-to-right horizontally, workspaces are arranged vertically.
Workspaces are dynamic and arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Features
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- Touchpad gesture to switch workspaces
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Touchpad gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
- Configurable layout: gaps, borders, struts, window sizes
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e)
- Live-reloading config
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
Note that NVIDIA GPUs may have issues.
## Inspiration
@@ -44,162 +57,11 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Building
> [!TIP]
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> For NixOS users, check out https://github.com/sodiboo/niri-flake
First, install the dependencies for your distribution.
- Ubuntu:
```sh
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
```
- Fedora:
```sh
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
```
Next, build niri with `cargo build --release`.
### NixOS/Nix
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
```
nix run --impure github:guibou/nixGL -- ./results/bin/niri
```
## Installation
The recommended way to install and run niri is as a standalone desktop session.
To do that, put files into the correct directories according to this table.
| File | Destination |
| ---- | ----------- |
| `target/release/niri` | `/usr/bin/` |
| `resources/niri-session` | `/usr/bin/` |
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
| `resources/niri.service` | `/usr/lib/systemd/user/` |
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
Doing this will make niri appear in GDM and, presumably, other display managers.
## Running
`cargo run --release`
Inside an existing desktop session, it will run in a window.
On a TTY, it will run natively.
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
### Session
If you followed the recommended installation steps above, niri should appear in your display manager.
Starting it from there will run niri as a desktop session.
The niri session will autostart apps through the systemd xdg-autostart target.
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
Niri also works with some parts of xdg-desktop-portal-gnome.
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
### 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>.
When running in a window, the Mod key is <kbd>Alt</kbd>.
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
| Hotkey | Description |
| ------ | ----------- |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column |
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>1</kbd><kbd>9</kbd> | Switch to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused column to a workspace by index |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
## Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
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 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
+18 -18
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1702918879,
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
@@ -28,11 +28,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1701411808,
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
"lastModified": 1709274179,
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@@ -62,11 +62,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1701697642,
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"type": "github"
},
"original": {
@@ -77,11 +77,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1702900294,
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"type": "github"
},
"original": {
@@ -103,11 +103,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1701372675,
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
"lastModified": 1709219524,
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
"type": "github"
},
"original": {
+10 -10
View File
@@ -39,22 +39,22 @@
pname = "niri";
version = self.rev or "dirty";
src = nix-filter.lib.filter {
root = ./.;
include = [
./src
./niri-config
./niri-ipc
./Cargo.toml
./Cargo.lock
./resources
];
src = nixpkgs.lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(builtins.match "resources" path == null) ||
((craneLib.filterCargoSources path type) &&
(builtins.match "niri-visual-tests" path == null));
};
nativeBuildInputs = with pkgs; [
pkg-config
autoPatchelfHook
clang
gdk-pixbuf
graphene
gtk4
libadwaita
];
buildInputs = with pkgs; [
+4 -1
View File
@@ -9,8 +9,11 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.6.2"
knuffel = "3.2.0"
miette = "5.10.0"
smithay.workspace = true
niri-ipc = { version = "0.1.5", path = "../niri-ipc" }
regex = "1.10.4"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+1442 -147
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -8,4 +8,9 @@ edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
[features]
clap = ["dep:clap"]
+355 -5
View File
@@ -2,26 +2,243 @@
#![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";
mod socket;
pub use socket::{Socket, SOCKET_PATH_ENV};
/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Request {
/// Request the version string for the running niri instance.
Version,
/// Request information about connected outputs.
Outputs,
/// Request information about the focused window.
FocusedWindow,
/// Perform an action.
Action(Action),
/// Respond with an error (for testing error handling).
ReturnError,
}
/// Response from niri to client.
#[derive(Debug, Serialize, Deserialize)]
/// Reply from niri to client.
///
/// Every request gets one reply.
///
/// * If an error had occurred, it will be an `Reply::Err`.
/// * If the request does not need any particular response, it will be
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
pub type Reply = Result<Response, String>;
/// Successful response from niri to client.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Response {
/// A request that does not need a response was handled successfully.
Handled,
/// The version string for the running niri instance.
Version(String),
/// Information about connected outputs.
///
/// Map from connector name to output info.
Outputs(HashMap<String, Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
}
/// 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,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
/// 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.
@@ -41,6 +258,14 @@ pub struct Output {
///
/// `None` if the output is disabled.
pub current_mode: Option<usize>,
/// Whether the output supports variable refresh rate.
pub vrr_supported: bool,
/// Whether variable refresh rate is enabled on the output.
pub vrr_enabled: bool,
/// Logical output information.
///
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
pub logical: Option<LogicalOutput>,
}
/// Output mode.
@@ -52,4 +277,129 @@ pub struct Mode {
pub height: u16,
/// Refresh rate in millihertz.
pub refresh_rate: u32,
/// Whether this mode is preferred by the monitor.
pub is_preferred: bool,
}
/// Logical output in the compositor's coordinate space.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct LogicalOutput {
/// Logical X position.
pub x: i32,
/// Logical Y position.
pub y: i32,
/// Width in logical pixels.
pub width: u32,
/// Height in logical pixels.
pub height: u32,
/// Scale factor.
pub scale: f64,
/// Transform.
pub transform: Transform,
}
/// Output transform, which goes counter-clockwise.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
/// Untransformed.
Normal,
/// Rotated by 90°.
#[serde(rename = "90")]
_90,
/// Rotated by 180°.
#[serde(rename = "180")]
_180,
/// Rotated by 270°.
#[serde(rename = "270")]
_270,
/// Flipped horizontally.
Flipped,
/// Rotated by 90° and flipped horizontally.
Flipped90,
/// Flipped vertically.
Flipped180,
/// Rotated by 270° and flipped horizontally.
Flipped270,
}
/// Toplevel window.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Window {
/// Title, if set.
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
}
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""#),
}
}
}
impl FromStr for Transform {
type Err = &'static str;
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(concat!(
r#"invalid transform, can be "90", "180", "270", "#,
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
)),
}
}
}
+63
View File
@@ -0,0 +1,63 @@
//! Helper for blocking communication over the niri socket.
use std::env;
use std::io::{self, Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::{Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Helper for blocking communication over the niri socket.
///
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: UnixStream,
}
impl Socket {
/// Connects to the default niri IPC socket.
///
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
/// [`SOCKET_PATH_ENV`] environment variable.
pub fn connect() -> io::Result<Self> {
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
)
})?;
Self::connect_to(socket_path)
}
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
Ok(Self { stream })
}
/// Sends a request to niri and returns the response.
///
/// Return values:
///
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
pub fn send(self, request: Request) -> io::Result<Reply> {
let Self { mut stream } = self;
let mut buf = serde_json::to_vec(&request).unwrap();
stream.write_all(&buf)?;
stream.shutdown(Shutdown::Write)?;
buf.clear();
stream.read_to_end(&mut buf)?;
let reply = serde_json::from_slice(&buf)?;
Ok(reply)
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "niri-visual-tests"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.5", path = ".." }
niri-config = { version = "0.1.5", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+14
View File
@@ -0,0 +1,14 @@
# niri-visual-tests
> [!NOTE]
>
> This is a development-only app, you shouldn't package it.
This app contains a number of hard-coded test scenarios for visual inspection.
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
The idea is to go through the test scenarios and check that everything *looks* right.
## Running
You will need recent GTK and libadwaita.
Then, `cargo run`.
+3
View File
@@ -0,0 +1,3 @@
.anim-control-bar {
padding: 12px;
}
@@ -0,0 +1,76 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::gradient::GradientRenderElement;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientAngle {
angle: f32,
prev_time: Duration,
}
impl GradientAngle {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientAngle {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.angle += delta.as_secs_f32() * PI;
if self.angle >= PI * 2. {
self.angle -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size);
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
self.angle - FRAC_PI_2,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,116 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::gradient::GradientRenderElement;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientArea {
progress: f32,
border: FocusRing,
prev_time: Duration,
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let mut border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
active_color: Color::new(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
});
border.set_active(true);
Self {
progress: 0.,
border,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientArea {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.progress += delta.as_secs_f32() * PI;
if self.progress >= PI * 2. {
self.progress -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let mut rv = Vec::new();
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size);
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
));
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
self.border.update(g_size, true);
rv.extend(
self.border
.render(
renderer,
Point::from(g_loc),
Scale::from(1.),
size.to_logical(1),
)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
g_area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
FRAC_PI_4,
)
.into_iter()
.map(|elem| Box::new(elem) as _),
);
rv
}
}
+233
View File
@@ -0,0 +1,233 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
use niri::layout::{LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::utils::{Logical, Physical, Size};
use super::TestCase;
use crate::test_window::TestWindow;
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
pub struct Layout {
output: Output,
windows: Vec<TestWindow>,
layout: niri::layout::Layout<TestWindow>,
start_time: Duration,
steps: HashMap<Duration, DynStepFn>,
}
impl Layout {
pub fn new(size: Size<i32, Logical>) -> Self {
let output = Output::new(
String::new(),
PhysicalProperties {
size: Size::from((size.w, size.h)),
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
},
);
let mode = Some(Mode {
size: size.to_physical(1),
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
active_gradient: None,
inactive_gradient: None,
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(options);
layout.add_output(output.clone());
Self {
output,
windows: Vec::new(),
layout,
start_time: get_monotonic_time(),
steps: HashMap::new(),
}
}
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.layout.activate_window(&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.id());
});
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.id());
});
}
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.id());
});
}
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.id());
});
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.id());
});
rv
}
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width), false);
window.communicate();
self.layout.add_window(window.clone(), width, false);
self.windows.push(window);
}
fn add_window_right_of(
&mut self,
right_of: &TestWindow,
mut window: TestWindow,
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width), false);
window.communicate();
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
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.id());
}
}
}
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, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::time::Duration;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
pub mod layout;
pub mod tile;
pub mod window;
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
false
}
fn advance_animations(&mut self, _current_time: Duration) {}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
}
+120
View File
@@ -0,0 +1,120 @@
use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
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, false);
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, false);
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, false);
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)), false);
self.window.communicate();
}
fn are_animations_ongoing(&self) -> bool {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time, true);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
self.tile
.render(
renderer,
location,
Scale::from(1.),
size.to_logical(1),
true,
RenderTarget::Output,
)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+64
View File
@@ -0,0 +1,64 @@
use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Window {
window: TestWindow,
}
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(size, false);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(size, false);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(size, false);
window.communicate();
Self { window }
}
}
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)), false);
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.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+170
View File
@@ -0,0 +1,170 @@
#[macro_use]
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::tile::Tile;
use cases::window::Window;
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use gtk::{gdk, gio, glib};
use niri::animation::ANIMATION_SLOWDOWN;
use smithay::utils::{Logical, Size};
use smithay_view::SmithayView;
use tracing_subscriber::EnvFilter;
use crate::cases::gradient_angle::GradientAngle;
use crate::cases::gradient_area::GradientArea;
use crate::cases::layout::Layout;
use crate::cases::TestCase;
mod cases;
mod smithay_view;
mod test_window;
fn main() -> glib::ExitCode {
let directives =
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
app.connect_startup(on_startup);
app.connect_activate(build_ui);
app.run()
}
fn on_startup(_app: &adw::Application) {
// Load our CSS.
let provider = gtk::CssProvider::new();
provider.load_from_string(include_str!("../resources/style.css"));
if let Some(display) = gdk::Display::default() {
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
struct S {
stack: gtk::Stack,
}
impl S {
fn add<T: TestCase + 'static>(
&self,
make: impl Fn(Size<i32, Logical>) -> T + 'static,
title: &str,
) {
let view = SmithayView::new(make);
self.stack.add_titled(&view, None, title);
}
}
let s = S {
stack: stack.clone(),
};
s.add(Window::freeform, "Freeform Window");
s.add(Window::fixed_size, "Fixed Size Window");
s.add(
Window::fixed_size_with_csd_shadow,
"Fixed Size Window - CSD Shadow",
);
s.add(Tile::freeform, "Freeform Tile");
s.add(Tile::fixed_size, "Fixed Size Tile");
s.add(
Tile::fixed_size_with_csd_shadow,
"Fixed Size Tile - CSD Shadow",
);
s.add(Tile::freeform_open, "Freeform Tile - Open");
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
s.add(
Tile::fixed_size_with_csd_shadow_open,
"Fixed Size Tile - CSD Shadow - Open",
);
s.add(Layout::open_in_between, "Layout - Open In-Between");
s.add(
Layout::open_multiple_quickly,
"Layout - Open Multiple Quickly",
);
s.add(
Layout::open_multiple_quickly_big,
"Layout - Open Multiple Quickly - Big",
);
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
s.add(
Layout::open_to_the_left_big,
"Layout - Open To The Left - Big",
);
s.add(GradientAngle::new, "Gradient - Angle");
s.add(GradientArea::new, "Gradient - Area");
let content_headerbar = adw::HeaderBar::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
anim_adjustment
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
anim_scale.set_hexpand(true);
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
anim_control_bar.add_css_class("anim-control-bar");
anim_control_bar.append(&gtk::Label::new(Some("Slowdown")));
anim_control_bar.append(&anim_scale);
let content_view = adw::ToolbarView::new();
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.add_top_bar(&content_headerbar);
content_view.add_bottom_bar(&anim_control_bar);
content_view.set_content(Some(&stack));
let content = adw::NavigationPage::new(
&content_view,
stack
.page(&stack.visible_child().unwrap())
.title()
.as_deref()
.unwrap(),
);
let sidebar_header = adw::HeaderBar::new();
let stack_sidebar = gtk::StackSidebar::new();
stack_sidebar.set_stack(&stack);
let sidebar_view = adw::ToolbarView::new();
sidebar_view.add_top_bar(&sidebar_header);
sidebar_view.set_content(Some(&stack_sidebar));
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
let split_view = adw::NavigationSplitView::new();
split_view.set_content(Some(&content));
split_view.set_sidebar(Some(&sidebar));
stack.connect_visible_child_notify(move |stack| {
content.set_title(
stack
.visible_child()
.and_then(|c| stack.page(&c).title())
.as_deref()
.unwrap_or_default(),
)
});
let window = adw::ApplicationWindow::new(app);
window.set_title(Some("niri visual tests"));
window.set_content(Some(&split_view));
window.present();
}
+251
View File
@@ -0,0 +1,251 @@
use gtk::glib;
use gtk::subclass::prelude::*;
use smithay::utils::{Logical, Size};
use crate::cases::TestCase;
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::render_helpers::{resources, shaders};
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
#[derive(Default)]
pub struct SmithayView {
gl_area: gtk::GLArea,
size: Cell<(i32, i32)>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SmithayView {
const NAME: &'static str = "NiriSmithayView";
type Type = super::SmithayView;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk::BinLayout>();
}
}
impl ObjectImpl for SmithayView {
fn constructed(&self) {
let obj = self.obj();
self.parent_constructed();
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
self.gl_area.set_parent(&*obj);
self.gl_area.connect_resize({
let imp = self.downgrade();
move |_, width, height| {
if let Some(imp) = imp.upgrade() {
imp.resize(width, height);
}
}
});
self.gl_area.connect_render({
let imp = self.downgrade();
move |_, gl_context| {
if let Some(imp) = imp.upgrade() {
if let Err(err) = imp.render(gl_context) {
warn!("error rendering: {err:?}");
}
}
glib::Propagation::Stop
}
});
obj.add_tick_callback(|obj, _frame_clock| {
let imp = obj.imp();
if let Some(case) = &mut *imp.test_case.borrow_mut() {
if case.are_animations_ongoing() {
imp.gl_area.queue_draw();
}
}
glib::ControlFlow::Continue
});
}
fn dispose(&self) {
self.gl_area.unparent();
}
}
impl WidgetImpl for SmithayView {
fn unmap(&self) {
self.test_case.replace(None);
self.parent_unmap();
}
fn unrealize(&self) {
self.renderer.replace(None);
self.parent_unrealize();
}
}
impl SmithayView {
fn resize(&self, width: i32, height: i32) {
self.size.set((width, height));
if let Some(case) = &mut *self.test_case.borrow_mut() {
case.resize(width, height);
}
}
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
// Set up the Smithay renderer.
let mut renderer = self.renderer.borrow_mut();
let renderer = renderer.get_or_insert_with(|| {
unsafe { create_renderer() }
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
});
let Ok(renderer) = renderer else {
return Ok(());
};
let size = self.size.get();
// Create the test case if missing.
let mut case = self.test_case.borrow_mut();
let case = case.get_or_insert_with(|| {
let make = self.make_test_case.get().unwrap();
make(Size::from(size))
});
case.advance_animations(get_monotonic_time());
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
case.render(renderer, Size::from(size))
})
}?;
let mut frame = renderer
.render(rect.size, Transform::Normal)
.context("error creating frame")?;
frame
.clear([0.3, 0.3, 0.3, 1.], &[rect])
.context("error clearing")?;
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
Ok(())
}
}
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
let egl_display = egl::GetCurrentDisplay();
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
let egl_context = egl::GetCurrentContext();
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
// really use it anyway so just get some random one.
let mut egl_config_id = null();
let mut num_configs = 0;
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
ensure!(res == egl::TRUE, "error choosing EGL config");
ensure!(num_configs != 0, "no EGL config");
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
.context("error creating EGL context")?;
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
.context("error getting supported renderer capabilities")?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
Ok(renderer)
}
unsafe fn with_framebuffer_save_restore<T>(
renderer: &mut GlesRenderer,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> anyhow::Result<T> {
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
let rv = f(renderer);
renderer.unbind().context("error unbinding")?;
renderer
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
Ok(rv)
}
}
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
}
}
+241
View File
@@ -0,0 +1,241 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::RenderTarget;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
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 {
id: usize,
inner: 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 {
id,
inner: Rc::new(RefCell::new(TestWindowInner {
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.inner.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
self.inner.borrow_mut().max_size = size;
}
pub fn set_color(&self, color: [f32; 4]) {
self.inner.borrow_mut().buffer.set_color(color);
}
pub fn set_csd_shadow_width(&self, width: i32) {
self.inner.borrow_mut().csd_shadow_width = width;
}
pub fn communicate(&self) -> bool {
let mut rv = false;
let mut inner = self.inner.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 LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.id
}
fn size(&self) -> Size<i32, Logical> {
self.inner.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>,
alpha: f32,
_target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
]
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.inner.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
self.inner.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 set_activated(&mut self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.inner.borrow().pending_fullscreen
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
fn take_unmap_snapshot(&self) -> Option<LayoutElementRenderSnapshot> {
None
}
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
None
}
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
None
}
}
+194 -108
View File
@@ -1,6 +1,11 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
@@ -11,16 +16,6 @@ input {
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
}
// You can set the keyboard repeat parameters. The defaults match wlroots and sway.
// Delay is in milliseconds before the repeat starts. Rate is in characters per second.
// repeat-delay 600
// repeat-rate 25
// Niri can remember the keyboard layout globally (the default) or per-window.
// - "global" - layout change is global for all windows.
// - "window" - layout is tracked for each window individually.
// track-layout "global"
}
// Next sections include libinput settings.
@@ -28,10 +23,10 @@ input {
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
}
mouse {
@@ -40,86 +35,73 @@ input {
// accel-profile "flat"
}
tablet {
// Set the name of the output (see below) which the tablet will map to.
// If this is unset or the output doesn't exist, the tablet maps to one of the
// existing outputs.
map-to-output "eDP-1"
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// By default, niri will take over the power button to make it sleep
// instead of power off.
// Uncomment this if you would like to configure the power button elsewhere
// (i.e. logind.conf).
// disable-power-key-handling
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
}
// 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 "/-"!
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
// off
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// Resolution and, optionally, refresh rate of the output.
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
// If the refresh rate is omitted, niri will pick the highest refresh rate
// for the resolution.
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@144"
mode "1920x1080@120.030"
// 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"
// Position of the output in the global coordinate space.
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
// The cursor can only move between directly adjacent outputs.
// Output scale has to be taken into account for positioning:
// Output scale and rotation has to be taken into account for positioning:
// outputs are sized in logical, or scaled, pixels.
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
// so to put another output directly adjacent to it on the right, set its x to 1920.
// It the position is unset or results in an overlap, the output is instead placed
// If the position is unset or results in an overlap, the output is instead placed
// automatically.
position x=1280 y=0
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// Set gaps around windows in logical pixels.
gaps 16
// How many logical pixels the ring extends out from the windows.
width 4
// Color of the ring on the active monitor: red, green, blue, alpha.
active-color 127 200 255 255
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
}
// 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.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.33333
proportion 0.5
proportion 0.667
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
@@ -130,8 +112,64 @@ layout {
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 4
// Colors can be set in a variety of ways:
// - CSS named colors: "red"
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
// Color of the ring on the active monitor.
active-color "#7fc8ff"
// Color of the ring on inactive monitors.
inactive-color "#505050"
// You can also use gradients. They take precedence over solid colors.
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
// The angle is the same as in linear-gradient, and is optional,
// defaulting to 180 (top-to-bottom gradient).
// You can use any CSS linear-gradient tool on the web to set these up.
//
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// You can also color the gradient relative to the entire view
// of the workspace, rather than relative to just the window itself.
// To do that, set relative-to="workspace-view".
//
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color "#ffc87f"
inactive-color "#505050"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
@@ -144,14 +182,6 @@ 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.
@@ -159,13 +189,6 @@ layout {
// which may be more convenient to use.
// spawn-at-startup "alacritty" "-e" "fish"
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
// xcursor-theme "default"
// xcursor-size 24
}
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
@@ -179,10 +202,41 @@ 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.
// The wiki explains how to configure individual animations:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
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
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
// Example: block out two password managers from screen capture.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
match app-id=r#"^org\.gnome\.World\.Secrets$"#
block-out-from "screen-capture"
// Use this instead if you want them visible on third-party screenshot tools.
// block-out-from "screencast"
}
binds {
@@ -192,6 +246,9 @@ binds {
//
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// when running as a winit window.
//
// Most actions that you can bind here can also be invoked programmatically with
// `niri msg action do-something`.
// Mod-Shift-/, which is usually the same as Mod-?,
// shows a list of important hotkeys.
@@ -200,14 +257,17 @@ binds {
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Mod+Alt+L { spawn "swaylock"; }
Super+Alt+L { spawn "swaylock"; }
// You can also use a shell:
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
// Example volume keys mappings for PipeWire & WirePlumber.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
// The allow-when-locked=true property makes them work even when the session is locked.
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
Mod+Q { close-window; }
@@ -263,6 +323,10 @@ binds {
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
// ...
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
@@ -281,6 +345,46 @@ binds {
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// You can bind mouse wheel scroll ticks using the following syntax.
// These binds will change direction based on the natural-scroll setting.
//
// To avoid scrolling through workspaces really fast, you can use
// the cooldown-ms property. The bind will be rate-limited to this value.
// You can set a cooldown on any bind, but it's most useful for the wheel.
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
// Usually scrolling up and down with Shift in applications results in
// horizontal scrolling; these binds replicate that.
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// Similarly, you can bind touchpad scroll "ticks".
// Touchpad scrolling is continuous, so for these binds it is split into
// discrete intervals.
// These binds are also affected by touchpad's natural-scroll, so these
// example binds are "inverted", since we have natural-scroll enabled for
// touchpads by default.
// Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
// Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
// You can refer to workspaces by index. However, keep in mind that
// niri is a dynamic workspace system, so these commands are kind of
// "best effort". Trying to refer to a workspace index bigger than
// the current workspace count will instead refer to the bottommost
// (empty) workspace.
//
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
// will all refer to the 3rd workspace.
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
@@ -303,9 +407,16 @@ binds {
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }
// There are also commands that consume or expel a single window to the side.
// Mod+BracketLeft { consume-or-expel-window-left; }
// Mod+BracketRight { consume-or-expel-window-right; }
Mod+R { switch-preset-column-width; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
@@ -338,35 +449,10 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
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
// Slow down animations by this factor.
// animation-slowdown 3.0
// Override the DRM device that niri will use for all rendering.
// render-drm-device "/dev/dri/renderD129"
}
+1 -7
View File
@@ -20,12 +20,6 @@ fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Set the current desktop for xdg-desktop-portal.
export XDG_CURRENT_DESKTOP=niri
# Ensure the session type is set to Wayland for xdg-autostart apps.
export XDG_SESSION_TYPE=wayland
# Import the login manager environment.
systemctl --user import-environment
@@ -44,4 +38,4 @@ systemctl --user --wait start niri.service
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
+2 -1
View File
@@ -9,5 +9,6 @@ Wants=xdg-desktop-autostart.target
Before=xdg-desktop-autostart.target
[Service]
Slice=session.slice
Type=notify
ExecStart=/usr/bin/niri
ExecStart=/usr/bin/niri --session
-58
View File
@@ -1,58 +0,0 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
duration: Duration,
start_time: Duration,
current_time: Duration,
}
impl Animation {
pub fn new(from: f64, to: f64, over: Duration) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
Self {
from,
to,
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
start_time: now,
current_time: now,
}
}
pub fn set_current_time(&mut self, time: Duration) {
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn value(&self) -> f64 {
let passed = (self.current_time - self.start_time).as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
EaseOutCubic.y(x) * (self.to - self.from) + self.from
}
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
}
+376
View File
@@ -0,0 +1,376 @@
use std::time::Duration;
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
initial_velocity: f64,
is_off: bool,
duration: Duration,
/// Time until the animation first reaches `to`.
///
/// Best effort; not always exactly precise.
clamped_duration: Duration,
start_time: Duration,
current_time: Duration,
kind: Kind,
}
#[derive(Debug, Clone, Copy)]
enum Kind {
Easing {
curve: Curve,
},
Spring(Spring),
Deceleration {
initial_velocity: f64,
deceleration_rate: f64,
},
}
#[derive(Debug, Clone, Copy)]
pub enum Curve {
EaseOutQuad,
EaseOutCubic,
EaseOutExpo,
}
impl Animation {
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
if config.off {
rv.is_off = true;
return rv;
}
rv.replace_config(config);
rv
}
pub fn replace_config(&mut self, config: niri_config::Animation) {
self.is_off = config.off;
if config.off {
self.duration = Duration::ZERO;
self.clamped_duration = Duration::ZERO;
return;
}
let start_time = self.start_time;
let current_time = self.current_time;
match config.kind {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
let spring = Spring {
from: self.from,
to: self.to,
initial_velocity: self.initial_velocity,
params,
};
*self = Self::spring(spring);
}
niri_config::AnimationKind::Easing(p) => {
*self = Self::ease(
self.from,
self.to,
self.initial_velocity,
u64::from(p.duration_ms),
Curve::from(p.curve),
);
}
}
self.start_time = start_time;
self.current_time = current_time;
}
/// Restarts the animation using the previous config.
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
if self.is_off {
return self;
}
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
match self.kind {
Kind::Easing { curve } => Self::ease(
from,
to,
initial_velocity,
self.duration.as_millis() as u64,
curve,
),
Kind::Spring(spring) => {
let spring = Spring {
from: self.from,
to: self.to,
initial_velocity: self.initial_velocity,
params: spring.params,
};
Self::spring(spring)
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let threshold = 0.001; // FIXME
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
}
}
}
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
Self {
from,
to,
initial_velocity,
is_off: false,
duration,
// Our current curves never overshoot.
clamped_duration: duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn spring(spring: Spring) -> Self {
let _span = tracy_client::span!("Animation::spring");
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
let kind = Kind::Spring(spring);
Self {
from: spring.from,
to: spring.to,
initial_velocity: spring.initial_velocity,
is_off: false,
duration,
clamped_duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn decelerate(
from: f64,
initial_velocity: f64,
deceleration_rate: f64,
threshold: f64,
) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration_s = if initial_velocity == 0. {
0.
} else {
let coeff = 1000. * deceleration_rate.ln();
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
};
let duration = Duration::from_secs_f64(duration_s);
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
let kind = Kind::Deceleration {
initial_velocity,
deceleration_rate,
};
Self {
from,
to,
initial_velocity,
is_off: false,
duration,
clamped_duration: duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn set_current_time(&mut self, time: Duration) {
if self.duration.is_zero() {
self.current_time = time;
return;
}
let end_time = self.start_time + self.duration;
if end_time <= self.current_time {
return;
}
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
if slowdown <= f64::EPSILON {
// Zero slowdown will cause the animation to end right away.
self.current_time = end_time;
return;
}
// We can't change current_time (since the incoming time values are always real-time), so
// apply the slowdown by shifting the start time to compensate.
if self.current_time <= time {
let delta = time - self.current_time;
let max_delta = end_time - self.current_time;
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
if slowdown <= min_slowdown {
// Our slowdown value will cause the animation to end right away.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time -= adjusted_delta - delta;
} else {
self.start_time += delta - adjusted_delta;
}
} else {
let delta = self.current_time - time;
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
if slowdown <= min_slowdown {
// Current time was about to jump to before the animation had started; let's just
// cancel the animation in this case.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time += adjusted_delta - delta;
} else {
self.start_time -= delta - adjusted_delta;
}
}
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn is_clamped_done(&self) -> bool {
self.current_time >= self.start_time + self.clamped_duration
}
pub fn value(&self) -> f64 {
if self.is_done() {
return self.to;
}
let passed = self.current_time - self.start_time;
match self.kind {
Kind::Easing { curve } => {
let passed = passed.as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
curve.y(x) * (self.to - self.from) + self.from
}
Kind::Spring(spring) => {
let value = spring.value_at(passed);
// Protect against numerical instability.
let range = (self.to - self.from) * 10.;
let a = self.from - range;
let b = self.to + range;
if self.from <= self.to {
value.clamp(a, b)
} else {
value.clamp(b, a)
}
}
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let passed = passed.as_secs_f64();
let coeff = 1000. * deceleration_rate.ln();
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
}
}
}
/// Returns a value that stops at the target value after first reaching it.
///
/// Best effort; not always exactly precise.
pub fn clamped_value(&self) -> f64 {
if self.is_clamped_done() {
return self.to;
}
self.value()
}
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
pub fn offset(&mut self, offset: f64) {
self.from += offset;
self.to += offset;
if let Kind::Spring(spring) = &mut self.kind {
spring.from += offset;
spring.to += offset;
}
}
}
impl Curve {
pub fn y(self, x: f64) -> f64 {
match self {
Curve::EaseOutQuad => EaseOutQuad.y(x),
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::EaseOutQuad => Curve::EaseOutQuad,
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
}
}
}
+168
View File
@@ -0,0 +1,168 @@
use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct SpringParams {
pub damping: f64,
pub mass: f64,
pub stiffness: f64,
pub epsilon: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct Spring {
pub from: f64,
pub to: f64,
pub initial_velocity: f64,
pub params: SpringParams,
}
impl SpringParams {
pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self {
let damping_ratio = damping_ratio.max(0.);
let stiffness = stiffness.max(0.);
let epsilon = epsilon.max(0.);
let mass = 1.;
let critical_damping = 2. * (mass * stiffness).sqrt();
let damping = damping_ratio * critical_damping;
Self {
damping,
mass,
stiffness,
epsilon,
}
}
}
impl Spring {
pub fn value_at(&self, t: Duration) -> f64 {
self.oscillate(t.as_secs_f64())
}
// Based on libadwaita (LGPL-2.1-or-later):
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c,
// which itself is based on (MIT):
// https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
/// Computes and returns the duration until the spring is at rest.
pub fn duration(&self) -> Duration {
const DELTA: f64 = 0.001;
let beta = self.params.damping / (2. * self.params.mass);
if beta.abs() <= f64::EPSILON || beta < 0. {
return Duration::MAX;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
// and general estimation for the oscillating ones
// we take the value of the envelope when it's < epsilon.
let mut x0 = -self.params.epsilon.ln() / beta;
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
return Duration::from_secs_f64(x0);
}
// Since the overdamped solution decays way slower than the envelope
// we need to use the value of the oscillation itself.
// Newton's root finding method is a good candidate in this particular case:
// https://en.wikipedia.org/wiki/Newton%27s_method
let mut y0 = self.oscillate(x0);
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
let mut x1 = (self.to - y0 + m * x0) / m;
let mut y1 = self.oscillate(x1);
let mut i = 0;
while (self.to - y1).abs() > self.params.epsilon {
if i > 1000 {
return Duration::ZERO;
}
x0 = x1;
y0 = y1;
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
i += 1;
}
Duration::from_secs_f64(x1)
}
/// Computes and returns the duration until the spring reaches its target position.
pub fn clamped_duration(&self) -> Option<Duration> {
let beta = self.params.damping / (2. * self.params.mass);
if beta.abs() <= f64::EPSILON || beta < 0. {
return Some(Duration::MAX);
}
if (self.to - self.from).abs() <= f64::EPSILON {
return Some(Duration::ZERO);
}
// The first frame is not that important and we avoid finding the trivial 0 for in-place
// animations.
let mut i = 1u16;
let mut y = self.oscillate(f64::from(i) / 1000.);
while (self.to - self.from > f64::EPSILON && self.to - y > self.params.epsilon)
|| (self.from - self.to > f64::EPSILON && y - self.to > self.params.epsilon)
{
if i > 3000 {
return None;
}
i += 1;
y = self.oscillate(f64::from(i) / 1000.);
}
Some(Duration::from_millis(u64::from(i)))
}
/// Returns the spring position at a given time in seconds.
fn oscillate(&self, t: f64) -> f64 {
let b = self.params.damping;
let m = self.params.mass;
let k = self.params.stiffness;
let v0 = self.initial_velocity;
let beta = b / (2. * m);
let omega0 = (k / m).sqrt();
let x0 = self.from - self.to;
let envelope = (-beta * t).exp();
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
// for the differential equation m*ẍ+b*ẋ+kx = 0
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
// Critically damped.
self.to + envelope * (x0 + (beta * x0 + v0) * t)
} else if beta < omega0 {
// Underdamped.
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
self.to
+ envelope
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
} else {
// Overdamped.
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
self.to
+ envelope
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
}
}
}
+6 -14
View File
@@ -1,6 +1,4 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -10,7 +8,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;
@@ -33,6 +31,8 @@ pub enum RenderResult {
Skipped,
}
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
match self {
@@ -98,7 +98,7 @@ impl Backend {
}
}
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self {
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
@@ -112,21 +112,13 @@ impl Backend {
}
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
match self {
Backend::Tty(tty) => tty.enabled_outputs(),
Backend::Winit(winit) => winit.enabled_outputs(),
}
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn gbm_device(
&self,
@@ -138,7 +130,7 @@ impl Backend {
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
+907 -242
View File
File diff suppressed because it is too large Load Diff
+58 -49
View File
@@ -16,29 +16,30 @@ 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::utils::get_monotonic_time;
use crate::Niri;
use super::{IpcOutputMap, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
config: Rc<RefCell<Config>>,
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Winit {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
let (backend, winit) = winit::init_from_builder(builder).unwrap();
let (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -54,11 +55,11 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let physical_properties = output.physical_properties();
let ipc_outputs = Rc::new(RefCell::new(HashMap::from([(
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
niri_ipc::Output {
name: output.name(),
@@ -69,16 +70,15 @@ impl Winit {
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,
is_preferred: true,
}],
current_mode: Some(0),
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
},
)])));
let enabled_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
output.clone(),
)])));
let damage_tracker = OutputDamageTracker::from_output(&output);
event_loop
@@ -95,45 +95,47 @@ impl Winit {
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;
{
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
let output = ipc_outputs.get_mut("winit").unwrap();
let mode = &mut output.modes[0];
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
if let Some(logical) = output.logical.as_mut() {
logical.width = size.w as u32;
logical.height = size.h as u32;
}
state.niri.ipc_outputs_changed = true;
}
state.niri.output_resized(winit.output.clone());
state.niri.output_resized(&winit.output);
}
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Focus(_) => (),
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::Redraw => state.niri.queue_redraw(&state.backend.winit().output),
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
Self {
Ok(Self {
config,
output,
backend,
damage_tracker,
ipc_outputs,
enabled_outputs,
}
})
}
pub fn init(&mut self, niri: &mut Niri) {
if let Err(err) = self
.backend
.renderer()
.bind_wl_display(&niri.display_handle)
{
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
}
niri.add_output(self.output.clone(), None);
resources::init(renderer);
shaders::init(renderer);
niri.add_output(self.output.clone(), None, false);
}
pub fn seat_name(&self) -> String {
@@ -151,7 +153,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
let elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Hand them over to winit.
self.backend.bind().unwrap();
@@ -172,10 +179,12 @@ impl Winit {
.wait_for_frame_completion_before_queueing
{
let _span = tracy_client::span!("wait for completion");
res.sync.wait();
if let Err(err) = res.sync.wait() {
warn!("error waiting for frame completion: {err:?}");
}
}
self.backend.submit(Some(&damage)).unwrap();
self.backend.submit(Some(damage)).unwrap();
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
let mode = output.current_mode().unwrap();
@@ -195,12 +204,16 @@ impl Winit {
let output_state = niri.output_state.get_mut(output).unwrap();
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => (),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
}
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
// right away instead.
if output_state.unfinished_animations_remain {
self.backend.window().request_redraw();
}
@@ -213,21 +226,17 @@ 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 ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.enabled_outputs.clone()
}
}
+68
View File
@@ -0,0 +1,68 @@
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use crate::utils::version;
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
pub struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
pub config: Option<PathBuf>,
/// Import environment globally to systemd and D-Bus, run D-Bus services.
///
/// Set this flag in a systemd service started by your display manager, or when running
/// manually as your main compositor instance. Do not set when running as a nested window, or
/// on a TTY as your non-main compositor instance, to avoid messing up the global environment.
#[arg(long)]
pub session: bool,
/// Command to run upon compositor startup.
#[arg(last = true)]
pub command: Vec<OsString>,
#[command(subcommand)]
pub subcommand: Option<Sub>,
}
#[derive(Subcommand)]
pub enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
pub enum Msg {
/// Print the version of the running niri instance.
Version,
/// List connected outputs.
Outputs,
/// Print information about the focused window.
FocusedWindow,
/// Perform an action.
Action {
#[command(subcommand)]
action: Action,
},
/// Request an error from the running niri instance.
RequestError,
}
+6 -20
View File
@@ -8,8 +8,7 @@ use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
@@ -224,7 +223,7 @@ pub enum RenderCursor {
},
}
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
#[derive(Default)]
pub struct CursorTextureCache {
@@ -238,12 +237,11 @@ impl CursorTextureCache {
pub fn get(
&self,
renderer: &mut GlesRenderer,
icon: CursorIcon,
scale: i32,
cursor: &XCursor,
idx: usize,
) -> Option<TextureBuffer<GlesTexture>> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,26 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
let buffer = TextureBuffer::from_memory(
renderer,
MemoryRenderBuffer::from_slice(
&frame.pixels_rgba,
Fourcc::Abgr8888,
Fourcc::Argb8888,
(frame.width as i32, frame.height as i32),
false,
scale,
Transform::Normal,
None,
);
match buffer {
Ok(x) => Some(x),
Err(err) => {
warn!("error creating a cursor texture: {err:?}");
None
}
}
)
})
.collect()
})[idx]
+166
View File
@@ -0,0 +1,166 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{dbus_interface, MessageHeader, Task};
use super::Start;
pub struct ScreenSaver {
is_inhibited: Arc<AtomicBool>,
is_broken: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
counter: u32,
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: MessageHeader<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
trace!(
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
hdr.sender()
);
let Ok(Some(name)) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
let mut inhibitors = self.inhibitors.lock().unwrap();
let mut cookie = None;
for _ in 0..3 {
// Start from 1 because some clients don't like 0.
self.counter = self.counter.wrapping_add(1);
if self.counter == 0 {
self.counter += 1;
}
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
entry.insert(name);
self.is_inhibited.store(true, Ordering::SeqCst);
cookie = Some(self.counter);
break;
}
}
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
}
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
trace!("fdo uninhibit, cookie: {cookie}");
let mut inhibitors = self.inhibitors.lock().unwrap();
if inhibitors.remove(&cookie).is_some() {
if inhibitors.is_empty() {
self.is_inhibited.store(false, Ordering::SeqCst);
}
Ok(())
} else {
Err(fdo::Error::Failed(String::from("invalid cookie")))
}
}
}
impl ScreenSaver {
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
Self {
is_inhibited,
is_broken: Arc::new(AtomicBool::new(false)),
inhibitors: Arc::new(Mutex::new(HashMap::new())),
counter: 0,
monitor_task: Arc::new(OnceLock::new()),
}
}
}
async fn monitor_disappeared_clients(
conn: &zbus::Connection,
is_inhibited: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
) -> anyhow::Result<()> {
let proxy = fdo::DBusProxy::new(conn)
.await
.context("error creating a DBusProxy")?;
let mut stream = proxy
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
.await
.context("error creating a NameOwnerChanged stream")?;
while let Some(signal) = stream.next().await {
let args = signal
.args()
.context("error retrieving NameOwnerChanged args")?;
let Some(name) = &**args.old_owner() else {
continue;
};
if args.new_owner().is_none() {
trace!("fdo ScreenSaver client disappeared: {name}");
let mut inhibitors = inhibitors.lock().unwrap();
inhibitors.retain(|_, owner| owner != name);
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
} else {
error!("non-null new_owner should've been filtered out");
}
}
Ok(())
}
impl Start for ScreenSaver {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let is_inhibited = self.is_inhibited.clone();
let is_broken = self.is_broken.clone();
let inhibitors = self.inhibitors.clone();
let monitor_task = self.monitor_task.clone();
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/freedesktop/ScreenSaver", self)?;
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
let async_conn = conn.inner();
let future = {
let conn = async_conn.clone();
async move {
if let Err(err) =
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
.await
{
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
is_broken.store(true, Ordering::SeqCst);
is_inhibited.store(false, Ordering::SeqCst);
inhibitors.lock().unwrap().clear();
}
}
};
let task = async_conn
.executor()
.spawn(future, "monitor disappearing clients");
monitor_task.set(task).unwrap();
Ok(conn)
}
}
-1
View File
@@ -1,6 +1,5 @@
use std::path::PathBuf;
use smithay::reexports::calloop;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
+11 -4
View File
@@ -1,9 +1,9 @@
use smithay::reexports::calloop;
use zbus::blocking::Connection;
use zbus::Interface;
use crate::niri::State;
pub mod freedesktop_screensaver;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
@@ -13,6 +13,7 @@ pub mod mutter_screen_cast;
#[cfg(feature = "xdp-gnome-screencast")]
use mutter_screen_cast::ScreenCast;
use self::freedesktop_screensaver::ScreenSaver;
use self::mutter_display_config::DisplayConfig;
use self::mutter_service_channel::ServiceChannel;
@@ -24,6 +25,7 @@ trait Start: Interface {
pub struct DBusServers {
pub conn_service_channel: Option<Connection>,
pub conn_display_config: Option<Connection>,
pub conn_screen_saver: Option<Connection>,
pub conn_screen_shot: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
@@ -45,9 +47,12 @@ impl DBusServers {
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.enabled_outputs());
let display_config = DisplayConfig::new(backend.ipc_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
@@ -62,7 +67,7 @@ impl DBusServers {
dbus.conn_screen_shot = try_start(screenshot);
#[cfg(feature = "xdp-gnome-screencast")]
{
if niri.pipewire.is_some() {
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -75,8 +80,10 @@ impl DBusServers {
}
})
.unwrap();
let screen_cast = ScreenCast::new(backend.enabled_outputs(), to_niri);
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}
}
+77 -30
View File
@@ -2,15 +2,15 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
pub struct DisplayConfig {
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
#[derive(Serialize, Type)]
@@ -53,12 +53,14 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let mut monitors: Vec<Monitor> = self
.enabled_outputs
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.keys()
.map(|c| {
.iter()
// Take only enabled outputs.
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
.map(|(c, output)| {
// Loosely matches the check in Mutter.
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
@@ -78,46 +80,91 @@ impl DisplayConfig {
OwnedValue::from(is_laptop_panel),
);
Monitor {
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let refresh = refresh_rate as f64 / 1000.;
Mode {
id: format!("{width}x{height}@{refresh:.3}"),
width: i32::from(width),
height: i32::from(height),
refresh_rate: refresh,
preferred_scale: 1.,
supported_scales: vec![1., 2., 3.],
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
modes[output.current_mode.unwrap()]
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
let monitor = Monitor {
names: (c.clone(), String::new(), String::new(), serial),
modes: vec![],
modes,
properties,
}
};
let logical = output.logical.as_ref().unwrap();
let transform = match logical.transform {
niri_ipc::Transform::Normal => 0,
niri_ipc::Transform::_90 => 1,
niri_ipc::Transform::_180 => 2,
niri_ipc::Transform::_270 => 3,
niri_ipc::Transform::Flipped => 4,
niri_ipc::Transform::Flipped90 => 5,
niri_ipc::Transform::Flipped180 => 6,
niri_ipc::Transform::Flipped270 => 7,
};
let logical_monitor = LogicalMonitor {
x: logical.x,
y: logical.y,
scale: logical.scale,
transform,
is_primary: false,
monitors: vec![monitor.names.clone()],
properties: HashMap::new(),
};
(monitor, logical_monitor)
})
.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");
let a_is_builtin = a.0.properties.contains_key("display-name");
let b_is_builtin = b.0.properties.contains_key("display-name");
a_is_builtin
.cmp(&b_is_builtin)
.reverse()
.then_with(|| a.names.0.cmp(&b.names.0))
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
});
let logical_monitors = monitors
.iter()
.map(|m| LogicalMonitor {
x: 0,
y: 0,
scale: 1.,
transform: 0,
is_primary: false,
monitors: vec![m.names.clone()],
properties: HashMap::new(),
})
.collect();
Ok((0, monitors, logical_monitors, HashMap::new()))
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
// FIXME: monitors-changed signal.
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { enabled_outputs }
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
}
}
+45 -19
View File
@@ -5,16 +5,16 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
#[derive(Clone)]
pub struct ScreenCast {
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
@@ -23,10 +23,11 @@ pub struct ScreenCast {
#[derive(Clone)]
pub struct Session {
id: usize,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
stopped: Arc<AtomicBool>,
}
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
@@ -48,16 +49,26 @@ struct RecordMonitorProperties {
#[derive(Clone)]
pub struct Stream {
output: Output,
// FIXME: update on scale changes and whatnot.
output: niri_ipc::Output,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
/// Position of the stream in logical coordinates.
position: (i32, i32),
/// Size of the stream in logical coordinates.
size: (i32, i32),
}
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
output: Output,
output: String,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
@@ -85,11 +96,7 @@ 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.enabled_outputs.clone(),
self.to_niri.clone(),
);
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
match server.at(&path, session.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -129,6 +136,11 @@ impl Session {
) {
debug!("stop");
if self.stopped.swap(true, Ordering::SeqCst) {
// Already stopped.
return;
}
Session::closed(&ctxt).await.unwrap();
if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast {
@@ -156,10 +168,14 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
if output.logical.is_none() {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
@@ -169,7 +185,7 @@ impl Session {
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -195,15 +211,24 @@ 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 logical = self.output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
}
}
}
impl ScreenCast {
pub fn new(
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
enabled_outputs,
ipc_outputs,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
@@ -228,14 +253,15 @@ impl Start for ScreenCast {
impl Session {
pub fn new(
id: usize,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
enabled_outputs,
ipc_outputs,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
stopped: Arc::new(AtomicBool::new(false)),
}
}
}
@@ -250,7 +276,7 @@ impl Drop for Session {
impl Stream {
pub fn new(
output: Output,
output: niri_ipc::Output,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
@@ -269,7 +295,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast {
session_id,
output: self.output.clone(),
output: self.output.name.clone(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
+20 -2
View File
@@ -7,10 +7,11 @@ use crate::utils::get_monotonic_time;
pub struct FrameClock {
last_presentation_time: Option<Duration>,
refresh_interval_ns: Option<NonZeroU64>,
vrr: bool,
}
impl FrameClock {
pub fn new(refresh_interval: Option<Duration>) -> Self {
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
@@ -21,6 +22,7 @@ impl FrameClock {
Self {
last_presentation_time: None,
refresh_interval_ns,
vrr,
}
}
@@ -29,6 +31,15 @@ impl FrameClock {
.map(|r| Duration::from_nanos(r.get()))
}
pub fn set_vrr(&mut self, vrr: bool) {
if self.vrr == vrr {
return;
}
self.vrr = vrr;
self.last_presentation_time = None;
}
pub fn presented(&mut self, presentation_time: Duration) {
if presentation_time.is_zero() {
// Not interested in these.
@@ -71,6 +82,13 @@ impl FrameClock {
let since_last_ns =
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
last_presentation_time + Duration::from_nanos(to_next_ns)
// If VRR is enabled and more than one frame passed since last presentation, assume that we
// can present immediately.
if self.vrr && to_next_ns > refresh_interval_ns {
now
} else {
last_presentation_time + Duration::from_nanos(to_next_ns)
}
}
}
+142 -22
View File
@@ -16,9 +16,9 @@ use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell;
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::niri::{ClientState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -75,7 +75,7 @@ impl CompositorHandler for State {
}
}
}
})
});
}
fn commit(&mut self, surface: &WlSurface) {
@@ -93,45 +93,145 @@ impl CompositorHandler for State {
root_surface = parent;
}
// Update the cached root surface.
self.niri
.root_surface
.insert(surface.clone(), root_surface.clone());
if surface == &root_surface {
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
// The toplevel got mapped.
let window = entry.remove();
let Unmapped { window, state } = entry.remove();
window.on_commit();
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
{
self.niri.queue_redraw(output);
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, is_full_width, output) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(rules, width, is_full_width, output)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None)
};
let parent = toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
// Only consider the parent if we configured the window for the same
// output.
//
// Normally when we're following the parent, the configured output will be
// None. If the configured output is set, that means it was set explicitly
// by a window rule or a fullscreen request.
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
})
.map(|(mapped, _)| mapped.window.clone());
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
let mapped = Mapped::new(window, rules, hook);
let window = mapped.window.clone();
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
} else {
self.niri.layout.add_window(mapped, width, is_full_width)
};
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
}
return;
}
// The toplevel remains unmapped.
let window = entry.get();
xdg_shell::send_initial_configure_if_needed(window.toplevel());
let unmapped = entry.get();
if unmapped.needs_initial_configure() {
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
self.queue_initial_configure(toplevel);
}
return;
}
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
let (window, output) = clone2(win_out);
window.on_commit();
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
// 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
});
// Must start the close animation before window.on_commit().
if !is_mapped {
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window);
});
}
window.on_commit();
if !is_mapped {
// The toplevel got unmapped.
//
// Test client: wleird-unmap.
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
self.niri.unmapped_windows.insert(surface.clone(), window);
self.niri.queue_redraw(output);
if was_active {
self.maybe_warp_cursor_to_focus();
}
// Newly-unmapped toplevels must perform the initial commit-configure sequence
// afresh.
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(&output);
return;
}
@@ -141,7 +241,7 @@ impl CompositorHandler for State {
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
self.niri.queue_redraw(output);
self.niri.queue_redraw(&output);
return;
}
@@ -150,10 +250,12 @@ impl CompositorHandler for State {
// This is a commit of a non-root or a non-toplevel root.
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((window, output)) = root_window_output.map(clone2) {
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.queue_redraw(output);
self.niri.queue_redraw(&output);
return;
}
@@ -161,7 +263,7 @@ impl CompositorHandler for State {
self.popups_handle_commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
}
}
@@ -186,13 +288,31 @@ impl CompositorHandler for State {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == surface {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
break;
}
}
}
}
}
fn destroyed(&mut self, surface: &WlSurface) {
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
// when that happens, so that the closing animation includes all these subsurfaces.
//
// Test client: alacritty with CSD.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
self.backend.with_primary_renderer(|renderer| {
mapped.store_unmap_snapshot_if_empty(renderer);
});
}
}
self.niri
.root_surface
.retain(|k, v| k != surface && v != surface);
}
}
impl BufferHandler for State {
+2 -2
View File
@@ -50,7 +50,7 @@ impl WlrLayerShellHandler for State {
None
};
if let Some(output) = output {
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
@@ -107,6 +107,6 @@ impl State {
}
drop(map);
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
+221 -15
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::backend::input::TabletToolDescriptor;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -20,7 +23,13 @@ use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
@@ -37,20 +46,29 @@ use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
};
use smithay::wayland::tablet_manager::TabletSeatHandler;
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
};
use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
use crate::utils::output_size;
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<State> {
&mut self.niri.seat_state
@@ -73,14 +91,36 @@ 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);
delegate_tablet_manager!(State);
delegate_pointer_gestures!(State);
delegate_relative_pointer!(State);
delegate_text_input_manager!(State);
impl TabletSeatHandler for State {
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) {
// FIXME: tablet tools should have their own cursors.
self.niri.cursor_manager.set_cursor_image(image);
// FIXME: granular.
self.niri.queue_redraw_all();
}
}
delegate_tablet_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
@@ -115,7 +155,7 @@ impl InputMethodHandler for State {
self.niri
.layout
.find_window_and_output(parent)
.map(|(window, _)| window.geometry())
.map(|(mapped, _)| mapped.window.geometry())
.unwrap_or_default()
}
}
@@ -189,6 +229,11 @@ impl DataControlHandler for State {
delegate_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
delegate_presentation!(State);
@@ -204,13 +249,10 @@ impl DmabufHandler for State {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
match self.backend.import_dmabuf(&dmabuf) {
Ok(_) => {
let _ = notifier.successful::<State>();
}
Err(_) => {
notifier.failed();
}
if self.backend.import_dmabuf(&dmabuf) {
let _ = notifier.successful::<State>();
} else {
notifier.failed();
}
}
}
@@ -268,7 +310,7 @@ impl SecurityContextHandler for State {
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
error!("error inserting client: {err}");
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
@@ -277,3 +319,167 @@ impl SecurityContextHandler for State {
}
}
delegate_security_context!(State);
impl IdleNotifierHandler for State {
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
&mut self.niri.idle_notifier_state
}
}
delegate_idle_notify!(State);
impl IdleInhibitHandler for State {
fn inhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.insert(surface);
}
fn uninhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.remove(&surface);
}
}
delegate_idle_inhibit!(State);
impl ForeignToplevelHandler for State {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
&mut self.niri.foreign_toplevel_state
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.queue_redraw_all();
}
}
fn close(&mut self, wl_surface: WlSurface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
mapped.toplevel().send_close();
}
}
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !mapped
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
}
delegate_foreign_toplevel!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, screencopy: Screencopy) {
if let Err(err) = self
.niri
.render_for_screencopy(&mut self.backend, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
}
}
}
delegate_screencopy!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.drm_lease_state
.as_mut()
.unwrap()
}
fn lease_request(
&mut self,
node: DrmNode,
request: DrmLeaseRequest,
) -> Result<DrmLeaseBuilder, LeaseRejected> {
debug!(
"Received lease request for {} connectors",
request.connectors.len()
);
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.lease_request(request)
}
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
debug!("Lease success");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.new_lease(lease);
}
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
debug!("Destroyed lease");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.remove_lease(lease_id);
}
}
delegate_drm_lease!(State);
delegate_viewporter!(State);
impl GammaControlHandler for State {
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState {
&mut self.niri.gamma_control_manager_state
}
fn get_gamma_size(&mut self, output: &Output) -> Option<u32> {
match self.backend.tty().get_gamma_size(output) {
Ok(0) => None, // Setting gamma is not supported.
Ok(size) => Some(size),
Err(err) => {
warn!(
"error getting gamma size for output {}: {err:?}",
output.name()
);
None
}
}
}
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()> {
match self.backend.tty().set_gamma(output, ramp) {
Ok(()) => Some(()),
Err(err) => {
warn!("error setting gamma for output {}: {err:?}", output.name());
None
}
}
}
}
delegate_gamma_control!(State);
+425 -70
View File
@@ -12,7 +12,11 @@ use smithay::reexports::wayland_server::protocol::wl_output;
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::compositor::{
add_pre_commit_hook, send_surface_state, with_states, BufferAssignment, HookId,
SurfaceAttributes,
};
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;
@@ -20,10 +24,15 @@ use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use crate::layout::workspace::ColumnWidth;
use crate::layout::LayoutElement as _;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -32,27 +41,8 @@ impl XdgShellHandler for State {
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface().clone();
let window = Window::new(surface);
// Tell the surface the preferred size and bounds for its likely output.
if let Some(ws) = self.niri.layout.active_workspace() {
ws.configure_new_window(&window);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
let config = self.niri.config.borrow();
if config.prefer_no_csd {
window.toplevel().with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// At the moment of creation, xdg toplevels must have no buffer.
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
let unmapped = Unmapped::new(Window::new_wayland_window(surface));
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
assert!(existing.is_none());
}
@@ -94,6 +84,15 @@ impl XdgShellHandler for State {
}
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
@@ -185,9 +184,11 @@ impl XdgShellHandler for State {
fn maximize_request(&mut self, surface: ToplevelSurface) {
// FIXME
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&surface) {
surface.send_configure();
}
}
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
@@ -196,46 +197,167 @@ impl XdgShellHandler for State {
fn fullscreen_request(
&mut self,
surface: ToplevelSurface,
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
if surface
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
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);
}
}
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
}
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
if let Some((window, _)) = self
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
.find_window_and_output(toplevel.wl_surface())
{
let window = window.clone();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = Some(requested_output);
// The required configure will be the initial configure.
}
InitialConfigureState::Configured { output, .. } => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = requested_output
.as_ref()
// If none requested, try currently configured output.
.or(output.as_ref())
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in fullscreen_request()");
toplevel.send_configure();
}
}
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = None;
// The required configure will be the initial configure.
}
InitialConfigureState::Configured {
width,
is_full_width,
output,
..
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in unfullscreen_request()");
toplevel.send_configure();
}
}
@@ -255,22 +377,49 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out.map(clone2) else {
let Some((mapped, output)) = win_out else {
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
// involving laptop going to sleep and resuming.
error!("toplevel missing from both unmapped_windows and layout");
return;
};
let window = mapped.window.clone();
let output = output.clone();
self.backend.with_primary_renderer(|renderer| {
mapped.store_unmap_snapshot_if_empty(renderer);
});
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window);
});
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
self.niri.queue_redraw(output);
if was_active {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
}
}
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
}
delegate_xdg_shell!(State);
@@ -323,14 +472,14 @@ impl KdeDecorationHandler for State {
&self.niri.kde_decoration_state
}
}
delegate_kde_decoration!(State);
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
if !initial_configure_sent(toplevel) {
toplevel.send_configure();
impl XdgForeignHandler for State {
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
&mut self.niri.xdg_foreign_state
}
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
@@ -345,6 +494,132 @@ fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
error!("window must be present in unmapped_windows in send_initial_configure()");
return;
};
let config = self.niri.config.borrow();
let rules =
ResolvedWindowRules::compute(&config.window_rules, WindowRef::Unmapped(unmapped));
let Unmapped { window, state } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
return;
};
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o));
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
wants_fullscreen
.as_ref()
.and_then(|x| x.as_ref())
// The monitor might not exist if the output was disconnected.
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if this is a dialog with a parent, to place it next to the parent.
let mon = mon.map(|mon| (mon, false)).or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
// If not, use the active monitor.
let mon = mon.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
// If we're following the parent, don't set the target output, so that when the window is
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|| rules.open_fullscreen == Some(true)
{
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
width = ws.resolve_default_width(rules.default_width);
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
};
toplevel.send_configure();
}
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
// Send the initial configure in an idle, in case the client sent some more info after the
// initial commit.
self.niri.event_loop.insert_idle(move |state| {
if !toplevel.alive() {
return;
}
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
if unmapped.needs_initial_configure() {
state.send_initial_configure(&toplevel);
}
}
});
}
/// Should be called on `WlSurface::commit`
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
self.niri.popups.commit(surface);
@@ -394,8 +669,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, window, output);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@@ -451,7 +726,9 @@ impl State {
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
for (popup, _) in PopupManager::popups_for_surface(
window.toplevel().expect("no x11 support").wl_surface(),
) {
match popup {
PopupKind::Xdg(ref popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
@@ -465,6 +742,34 @@ impl State {
}
}
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
let config = self.niri.config.borrow();
let window_rules = &config.window_rules;
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
let new_rules =
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = new_rules;
}
} else if let Some((mapped, output)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
{
if mapped.recompute_window_rules(window_rules) {
drop(config);
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
fn unconstrain_with_padding(
@@ -507,3 +812,53 @@ fn unconstrain_with_padding(
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target)
}
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
let _span = tracy_client::span!("mapped toplevel pre-commit");
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
let (got_unmapped, commit_serial) = with_states(surface, |states| {
let attrs = states.cached_state.pending::<SurfaceAttributes>();
let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed));
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
(got_unmapped, role.configure_serial)
});
let animate = if let Some(serial) = commit_serial {
mapped.should_animate_commit(serial)
} else {
error!("commit on a mapped surface without a configured serial");
false
};
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
mapped.store_unmap_snapshot_if_empty(renderer);
});
} else {
// The toplevel remains mapped; clear any stored unmap snapshot.
let _ = mapped.take_unmap_snapshot();
if animate {
state.backend.with_primary_renderer(|renderer| {
mapped.store_animation_snapshot(renderer);
});
let window = mapped.window.clone();
state.niri.layout.prepare_for_resize_animation(&window);
}
}
})
}
+968 -142
View File
File diff suppressed because it is too large Load Diff
+176 -39
View File
@@ -1,46 +1,101 @@
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::{LogicalOutput, Mode, Output, Request, Response, Socket, Transform};
use serde_json::json;
use anyhow::{bail, Context};
use niri_ipc::{Mode, Output, Request, Response};
use crate::Msg;
use crate::cli::Msg;
use crate::utils::version;
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
format!(
"{} is not set, are you running this within niri?",
niri_ipc::SOCKET_PATH_ENV
)
let request = match &msg {
Msg::Version => Request::Version,
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::Action { action } => Request::Action(action.clone()),
Msg::RequestError => Request::ReturnError,
};
let socket = Socket::connect().context("error connecting to the niri socket")?;
let reply = socket
.send(request)
.context("error communicating with niri")?;
let compositor_version = match reply {
Err(_) if !matches!(msg, Msg::Version) => {
// If we got an error, it might be that the CLI is a different version from the running
// niri instance. Request the running instance version to compare and print a message.
Socket::connect()
.and_then(|socket| socket.send(Request::Version))
.ok()
}
_ => None,
};
// Default SIGPIPE so that our prints don't panic on stdout closing.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let response = reply.map_err(|err_msg| {
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request.
// Don't add irrelevant context.
}
}
anyhow!(err_msg).context("niri returned an error")
})?;
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match msg {
Msg::Outputs => Request::Outputs,
};
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 response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
match msg {
Msg::RequestError => {
bail!("unexpected response: expected an error, got {response:?}");
}
Msg::Version => {
let Response::Version(compositor_version) = response else {
bail!("unexpected response: expected Version, got {response:?}");
};
let cli_version = version();
if json {
println!(
"{}",
json!({
"compositor": compositor_version,
"cli": cli_version,
})
);
return Ok(());
}
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
println!("Compositor version: {compositor_version}");
println!("CLI version: {cli_version}");
}
Msg::Outputs => {
#[allow(irrefutable_let_patterns)]
let Response::Outputs(outputs) = response
else {
let Response::Outputs(outputs) = response else {
bail!("unexpected response: expected Outputs, got {response:?}");
};
@@ -62,6 +117,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
physical_size,
modes,
current_mode,
vrr_supported,
vrr_enabled,
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
@@ -74,32 +132,111 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else {
println!(" Disabled");
}
if vrr_supported {
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
println!(" Variable refresh rate: supported, {enabled}");
} else {
println!(" Variable refresh rate: not supported");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
println!(" Physical size: unknown");
}
if let Some(logical) = logical {
let LogicalOutput {
x,
y,
width,
height,
scale,
transform,
} = logical;
println!(" Logical position: {x}, {y}");
println!(" Logical size: {width}x{height}");
println!(" Scale: {scale}");
let transform = match transform {
Transform::Normal => "normal",
Transform::_90 => "90° counter-clockwise",
Transform::_180 => "180°",
Transform::_270 => "270° counter-clockwise",
Transform::Flipped => "flipped horizontally",
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
Transform::Flipped180 => "flipped vertically",
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
};
println!(" Transform: {transform}");
}
println!(" Available modes:");
for mode in modes {
for (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" {width}x{height}@{refresh:.3}");
let is_current = Some(idx) == current_mode;
let qualifier = match (is_current, is_preferred) {
(true, true) => " (current, preferred)",
(true, false) => " (current)",
(false, true) => " (preferred)",
(false, false) => "",
};
println!(" {width}x{height}@{refresh:.3}{qualifier}");
}
println!();
}
}
Msg::FocusedWindow => {
let Response::FocusedWindow(window) = response else {
bail!("unexpected response: expected FocusedWindow, got {response:?}");
};
if json {
let window = serde_json::to_string(&window).context("error formatting response")?;
println!("{window}");
return Ok(());
}
if let Some(window) = window {
println!("Focused window:");
if let Some(title) = window.title {
println!(" Title: \"{title}\"");
} else {
println!(" Title: (unset)");
}
if let Some(app_id) = window.app_id {
println!(" App ID: \"{app_id}\"");
} else {
println!(" App ID: (unset)");
}
} else {
println!("No window is focused.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
}
Ok(())
+64 -16
View File
@@ -1,8 +1,6 @@
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::sync::{Arc, Mutex};
use std::{env, io, process};
use anyhow::Context;
@@ -10,19 +8,26 @@ use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{Request, Response};
use niri_ipc::{Reply, Request, Response};
use smithay::desktop::Window;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::backend::IpcOutputMap;
use crate::niri::State;
use crate::utils::version;
pub struct IpcServer {
pub socket_path: PathBuf,
}
struct ClientCtx {
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
event_loop: LoopHandle<'static, State>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
ipc_focused_window: Arc<Mutex<Option<Window>>>,
}
impl IpcServer {
@@ -85,7 +90,9 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
};
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
ipc_outputs: state.backend.ipc_outputs(),
ipc_focused_window: state.niri.ipc_focused_window.clone(),
};
let future = async move {
@@ -108,20 +115,61 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
.await
.context("error reading request")?;
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
let request = serde_json::from_str(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let response = match request {
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
Response::Outputs(ipc_outputs)
let reply = request.and_then(|request| process(&ctx, request));
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
};
}
let buf = serde_json::to_vec(&response).context("error formatting response")?;
write
.write_all(&buf)
.await
.context("error writing response")?;
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, request: Request) -> Reply {
let response = match request {
Request::ReturnError => return Err(String::from("example compositor error")),
Request::Version => Response::Version(version()),
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
Response::Outputs(ipc_outputs)
}
Request::FocusedWindow => {
let window = ctx.ipc_focused_window.lock().unwrap().clone();
let window = window.map(|window| {
let wl_surface = window.toplevel().expect("no X11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
title: role.title.clone(),
app_id: role.app_id.clone(),
}
})
});
Response::FocusedWindow(window)
}
Request::Action(action) => {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action, false);
});
Response::Handled
}
};
Ok(response)
}
+164
View File
@@ -0,0 +1,164 @@
use std::time::Duration;
use anyhow::Context as _;
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Logical, Point, Scale, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
#[derive(Debug)]
pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer<GlesTexture>,
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer<GlesTexture>,
/// Where the window should be blocked out from.
block_out_from: Option<BlockOutFrom>,
/// Center of the window geometry.
center: Point<i32, Logical>,
/// Position in the workspace.
pos: Point<i32, Logical>,
/// How much the buffer should be offset.
buffer_offset: Point<i32, Logical>,
/// How much the blocked-out buffer should be offset.
blocked_out_buffer_offset: Point<i32, Logical>,
/// The closing animation.
anim: Animation,
/// Alpha the animation should start from.
starting_alpha: f32,
/// Scale the animation should start from.
starting_scale: f64,
}
niri_render_elements! {
ClosingWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
}
}
impl ClosingWindow {
#[allow(clippy::too_many_arguments)]
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: i32,
center: Point<i32, Logical>,
pos: Point<i32, Logical>,
anim: Animation,
starting_alpha: f32,
starting_scale: f64,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
let mut render_to_buffer = |elements: Vec<E>| -> anyhow::Result<_> {
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
Scale::from(scale as f64),
Transform::Normal,
Fourcc::Abgr8888,
&elements,
)
.context("error rendering to texture")?;
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let offset = geo.loc.to_logical(scale);
Ok((buffer, offset))
};
let (buffer, buffer_offset) =
render_to_buffer(snapshot.contents).context("error rendering contents")?;
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_buffer(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
center,
pos,
buffer_offset,
blocked_out_buffer_offset,
anim,
starting_alpha,
starting_scale,
})
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn are_animations_ongoing(&self) -> bool {
!self.anim.is_clamped_done()
}
pub fn render(
&self,
view_pos: i32,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let val = self.anim.clamped_value();
let block_out = match self.block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
};
let (buffer, offset) = if block_out {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.buffer, self.buffer_offset)
};
let elem = TextureRenderElement::from_texture_buffer(
Point::from((0., 0.)),
buffer,
Some(val.clamp(0., 1.) as f32 * self.starting_alpha),
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let elem = RescaleRenderElement::from_element(
elem,
(self.center - offset).to_physical_precise_round(scale),
((val / 5. + 0.8) * self.starting_scale).max(0.),
);
let mut location = self.pos + offset;
location.x -= view_pos;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
);
elem.into()
}
}
+99 -50
View File
@@ -1,64 +1,72 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{self, Color};
use niri_config::GradientRelativeTo;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use crate::niri_render_elements;
use crate::render_helpers::gradient::GradientRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
is_off: bool,
sizes: [Size<i32, Logical>; 4],
full_size: Size<i32, Logical>,
is_active: bool,
is_border: bool,
width: i32,
active_color: Color,
inactive_color: Color,
config: niri_config::FocusRing,
}
pub type FocusRingRenderElement = SolidColorRenderElement;
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = GradientRenderElement,
}
}
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
is_off: config.off,
sizes: Default::default(),
full_size: Default::default(),
is_active: false,
is_border: false,
width: config.width.into(),
active_color: config.active_color,
inactive_color: config.inactive_color,
config,
}
}
pub fn update_config(&mut self, config: niri_config::FocusRing) {
self.is_off = config.off;
self.width = config.width.into();
self.active_color = config.active_color;
self.inactive_color = config.inactive_color;
self.config = config;
}
pub fn update(
&mut self,
win_pos: Point<i32, Logical>,
win_size: Size<i32, Logical>,
is_border: bool,
) {
if is_border {
self.buffers[0].resize((win_size.w + self.width * 2, self.width));
self.buffers[1].resize((win_size.w + self.width * 2, self.width));
self.buffers[2].resize((self.width, win_size.h));
self.buffers[3].resize((self.width, win_size.h));
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
self.locations[0] = win_pos + Point::from((-self.width, -self.width));
self.locations[1] = win_pos + Point::from((-self.width, win_size.h));
self.locations[2] = win_pos + Point::from((-self.width, 0));
self.locations[3] = win_pos + Point::from((win_size.w, 0));
if is_border {
self.sizes[0] = Size::from((win_size.w + width * 2, width));
self.sizes[1] = Size::from((win_size.w + width * 2, width));
self.sizes[2] = Size::from((width, win_size.h));
self.sizes[3] = Size::from((width, win_size.h));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
let size = win_size + Size::from((self.width * 2, self.width * 2));
self.buffers[0].resize(size);
self.locations[0] = win_pos - Point::from((self.width, self.width));
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
}
self.is_border = is_border;
@@ -66,50 +74,91 @@ impl FocusRing {
pub fn set_active(&mut self, is_active: bool) {
let color = if is_active {
self.active_color.into()
self.config.active_color.into()
} else {
self.inactive_color.into()
self.config.inactive_color.into()
};
for buf in &mut self.buffers {
buf.set_color(color);
}
self.is_active = is_active;
}
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.is_off {
if self.config.off {
return rv.into_iter();
}
let mut push = |buffer, location: Point<i32, Logical>| {
let elem = SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
);
let gradient = if self.is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
let elem = gradient.and_then(|gradient| {
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
GradientRenderElement::new(
renderer,
scale,
Rectangle::from_loc_and_size(location, size),
gradient_area,
gradient.from.into(),
gradient.to.into(),
((gradient.angle as f32) - 90.).to_radians(),
)
.map(Into::into)
});
let elem = elem.unwrap_or_else(|| {
SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
rv.push(elem);
};
if self.is_border {
for (buf, loc) in zip(&self.buffers, self.locations) {
push(buf, loc);
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
push(buf, location + loc, size);
}
} else {
push(&self.buffers[0], self.locations[0]);
push(
&self.buffers[0],
location + self.locations[0],
self.sizes[0],
);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.width
self.config.width.into()
}
pub fn is_off(&self) -> bool {
self.is_off
self.config.off
}
}
+971 -231
View File
File diff suppressed because it is too large Load Diff
+272 -42
View File
@@ -2,22 +2,33 @@ 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, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::output_size;
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
stiffness: 0.5,
limit: 0.05,
};
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
@@ -26,6 +37,8 @@ pub struct Monitor<W: LayoutElement> {
pub workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
/// ID of the previously active workspace.
pub previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
@@ -44,6 +57,7 @@ pub struct WorkspaceSwitchGesture {
pub center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
pub tracker: SwipeTracker,
}
pub type MonitorRenderElement<R> =
@@ -57,6 +71,13 @@ impl WorkspaceSwitch {
}
}
pub fn target_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.to(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
@@ -72,11 +93,16 @@ impl<W: LayoutElement> Monitor<W> {
output,
workspaces,
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
options,
}
}
pub fn active_workspace_ref(&self) -> &Workspace<W> {
&self.workspaces[self.active_workspace_idx]
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
@@ -86,18 +112,22 @@ impl<W: LayoutElement> Monitor<W> {
return;
}
// FIXME: also compute and use current velocity.
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
current_idx,
idx as f64,
Duration::from_millis(250),
0.,
self.options.animations.workspace_switch.0,
)));
}
@@ -127,6 +157,26 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace_idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(right_of))
.unwrap();
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window_right_of(right_of, window, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
@@ -216,6 +266,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();
}
@@ -287,8 +345,9 @@ impl<W: LayoutElement> Monitor<W> {
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
let window = workspace
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
.into_window();
self.add_window(new_idx, window, true, width, is_full_width);
}
@@ -309,8 +368,9 @@ impl<W: LayoutElement> Monitor<W> {
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
let window = workspace
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
.into_window();
self.add_window(new_idx, window, true, width, is_full_width);
}
@@ -331,8 +391,9 @@ impl<W: LayoutElement> Monitor<W> {
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
let window = workspace
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
.into_window();
self.add_window(new_idx, window, true, width, is_full_width);
@@ -409,6 +470,11 @@ impl<W: LayoutElement> Monitor<W> {
));
}
fn previous_workspace_idx(&self) -> Option<usize> {
let id = self.previous_workspace_id?;
self.workspaces.iter().position(|w| w.id() == id)
}
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
// Don't animate this action.
@@ -417,6 +483,24 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let idx = min(idx, self.workspaces.len() - 1);
if idx == self.active_workspace_idx {
if let Some(prev_idx) = self.previous_workspace_idx() {
self.switch_workspace(prev_idx);
}
} else {
self.switch_workspace(idx);
}
}
pub fn switch_workspace_previous(&mut self) {
if let Some(idx) = self.previous_workspace_idx() {
self.switch_workspace(idx);
}
}
pub fn consume_into_column(&mut self) {
self.active_workspace().consume_into_column();
}
@@ -462,7 +546,10 @@ impl<W: LayoutElement> Monitor<W> {
pub fn are_transitions_ongoing(&self) -> bool {
self.workspace_switch.is_some()
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
|| self
.workspaces
.iter()
.any(|ws| ws.are_transitions_ongoing())
}
pub fn update_config(&mut self, options: Rc<Options>) {
@@ -512,8 +599,10 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
@@ -532,12 +621,33 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
if let Some(switch) = &self.workspace_switch {
let size = output_size(&self.output);
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = (offset * size.h as f64).round() as i32;
let clip_rect = Rectangle::from_loc_and_size((0, -offset), size);
rect = rect.intersection(clip_rect)?;
}
Some(rect)
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
@@ -547,14 +657,28 @@ impl<W: LayoutElement> Monitor<W> {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor() as usize;
let after_idx = render_idx.ceil() as usize;
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
(before_idx, Point::from((0, offset)))
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0, offset)))
} else {
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0, -size.h + offset)))
};
@@ -578,16 +702,12 @@ 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,
{
target: RenderTarget,
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
@@ -598,40 +718,67 @@ impl Monitor<Window> {
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor() as usize;
let after_idx = render_idx.ceil() as usize;
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
let before = self.workspaces[before_idx].render_elements(renderer);
let after = self.workspaces[after_idx].render_elements(renderer);
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return vec![];
}
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
let after = self.workspaces[after_idx].render_elements(renderer, target);
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: crop to infinite bounds for all sides except the side
// where the workspaces join,
// otherwise it will cut pixel shaders and mess up
// the coordinate space.
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
if before_idx < 0. {
return after.collect();
}
Some(after)
} else {
None
};
let before_idx = before_idx as usize;
let before = self.workspaces[before_idx].render_elements(renderer, target);
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities((0, offset), (size.w, size.h)),
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, size.h),
),
)?,
(0, -offset),
Relocate::Relative,
))
});
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities((0, 0), (size.w, offset)),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
before.chain(after).collect()
before.chain(after.into_iter().flatten()).collect()
}
None => {
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
let elements =
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
elements
.into_iter()
.filter_map(|elem| {
@@ -656,4 +803,87 @@ impl Monitor<Window> {
}
}
}
pub fn workspace_switch_gesture_begin(&mut self) {
let center_idx = self.active_workspace_idx;
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(center_idx as f64);
let gesture = WorkspaceSwitchGesture {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn workspace_switch_gesture_update(
&mut self,
delta_y: f64,
timestamp: Duration,
) -> Option<bool> {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None;
};
gesture.tracker.push(delta_y, timestamp);
let pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
if gesture.current_idx == new_idx {
return Some(false);
}
gesture.current_idx = new_idx;
Some(true)
}
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
if cancelled {
self.workspace_switch = None;
self.clean_up_workspaces();
return true;
}
let mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
let new_idx = new_idx.round() as usize;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
min,
max,
gesture.center_idx as f64 + current_pos,
);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
gesture.current_idx,
new_idx as f64,
velocity,
self.options.animations.workspace_switch.0,
)));
true
}
}
+516 -37
View File
@@ -2,15 +2,27 @@ use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use smithay::backend::allocator::Fourcc;
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::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::FocusRing;
use super::workspace::WorkspaceRenderElement;
use super::{LayoutElement, Options};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::crossfade::CrossfadeRenderElement;
use crate::render_helpers::offscreen::OffscreenRenderElement;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shaders::Shaders;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget, ToRenderElement};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -21,6 +33,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 +51,80 @@ 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>,
/// The animation of the window resizing.
resize_animation: Option<ResizeAnimation>,
/// The animation of a tile visually moving horizontally.
move_x_animation: Option<MoveAnimation>,
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// Configurable properties of the layout.
options: Rc<Options>,
pub options: Rc<Options>,
}
niri_render_elements! {
TileRenderElement<R> => {
LayoutElement = LayoutElementRenderElement<R>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
Crossfade = CrossfadeRenderElement,
}
}
niri_render_elements! {
TileSnapshotContentsRenderElement => {
Texture = PrimaryGpuTextureRenderElement,
SolidColor = SolidColorRenderElement,
}
}
niri_render_elements! {
TileSnapshotRenderElement => {
Contents = RescaleRenderElement<TileSnapshotContentsRenderElement>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
}
}
#[derive(Debug)]
struct ResizeAnimation {
anim: Animation,
size_from: Size<i32, Logical>,
snapshot: LayoutElementRenderSnapshot,
}
#[derive(Debug)]
struct MoveAnimation {
anim: Animation,
from: i32,
}
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,
resize_animation: None,
move_x_animation: None,
move_y_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;
}
@@ -59,26 +133,182 @@ impl<W: LayoutElement> Tile<W> {
if self.fullscreen_size != Size::from((0, 0)) {
self.is_fullscreen = self.window.is_fullscreen();
}
if let Some(animate_from) = self.window.take_animation_snapshot() {
let size_from = if let Some(resize) = self.resize_animation.take() {
// Compute like in animated_window_size(), but using the snapshot geometry (since
// the current one is already overwritten).
let mut size = animate_from.size;
let val = resize.anim.value();
let size_from = resize.size_from;
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
size
} else {
animate_from.size
};
let change = self.window.size().to_point() - size_from.to_point();
let change = max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.0);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
snapshot: animate_from,
});
} else {
self.resize_animation = None;
}
}
}
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
let width = self.border.width();
self.border.update(
(width, width).into(),
self.window.size(),
self.window.has_ssd(),
);
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
if let Some(anim) = &mut self.open_animation {
anim.set_current_time(current_time);
if anim.is_done() {
self.open_animation = None;
}
}
if let Some(resize) = &mut self.resize_animation {
resize.anim.set_current_time(current_time);
if resize.anim.is_done() {
self.resize_animation = None;
}
}
if let Some(move_) = &mut self.move_x_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_x_animation = None;
}
}
if let Some(move_) = &mut self.move_y_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_y_animation = None;
}
}
let draw_border_with_background = self
.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
self.border
.update(self.animated_window_size(), !draw_border_with_background);
self.border.set_active(is_active);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
false
} else {
draw_border_with_background
};
self.focus_ring
.update(self.animated_tile_size(), !draw_focus_ring_with_background);
self.focus_ring.set_active(is_active);
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_animation.is_some()
|| self.resize_animation.is_some()
|| self.move_x_animation.is_some()
|| self.move_y_animation.is_some()
}
pub fn render_offset(&self) -> Point<i32, Logical> {
let mut offset = Point::from((0., 0.));
if let Some(move_) = &self.move_x_animation {
offset.x += f64::from(move_.from) * move_.anim.value();
}
if let Some(move_) = &self.move_y_animation {
offset.y += f64::from(move_.from) * move_.anim.value();
}
offset.to_i32_round()
}
pub fn start_open_animation(&mut self) {
self.open_animation = Some(Animation::new(
0.,
1.,
0.,
self.options.animations.window_open.0,
));
}
pub fn open_animation(&self) -> &Option<Animation> {
&self.open_animation
}
pub fn resize_animation(&self) -> Option<&Animation> {
self.resize_animation.as_ref().map(|resize| &resize.anim)
}
pub fn animate_move_from(&mut self, from: Point<i32, Logical>) {
self.animate_move_x_from(from.x);
self.animate_move_y_from(from.y);
}
pub fn animate_move_x_from(&mut self, from: i32) {
self.animate_move_x_from_with_config(from, self.options.animations.window_movement.0);
}
pub fn animate_move_x_from_with_config(&mut self, from: i32, config: niri_config::Animation) {
let current_offset = self.render_offset().x;
// Preserve the previous config if ongoing.
let anim = self.move_x_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
self.move_x_animation = Some(MoveAnimation {
anim,
from: from + current_offset,
});
}
pub fn animate_move_y_from(&mut self, from: i32) {
self.animate_move_y_from_with_config(from, self.options.animations.window_movement.0);
}
pub fn animate_move_y_from_with_config(&mut self, from: i32, config: niri_config::Animation) {
let current_offset = self.render_offset().y;
// Preserve the previous config if ongoing.
let anim = self.move_y_animation.take().map(|move_| move_.anim);
let anim = anim
.map(|anim| anim.restarted(1., 0., 0.))
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
self.move_y_animation = Some(MoveAnimation {
anim,
from: from + current_offset,
});
}
pub fn window(&self) -> &W {
&self.window
}
pub fn window_mut(&mut self) -> &mut W {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
pub fn is_fullscreen(&self) -> bool {
self.is_fullscreen
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<i32> {
if self.is_fullscreen {
@@ -141,6 +371,41 @@ impl<W: LayoutElement> Tile<W> {
self.window.size()
}
fn animated_window_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
if let Some(resize) = &self.resize_animation {
let val = resize.anim.value();
let size_from = resize.size_from;
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
size.w = max(1, size.w);
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
size.h = max(1, size.h);
}
size
}
fn animated_tile_size(&self) -> Size<i32, Logical> {
let mut size = self.animated_window_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = max(size.w, self.fullscreen_size.w);
size.h = max(size.h, self.fullscreen_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
}
size
}
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
loc += self.window_loc();
@@ -158,7 +423,7 @@ impl<W: LayoutElement> Tile<W> {
activation_region.to_f64().contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>, animate: bool) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
@@ -166,7 +431,7 @@ impl<W: LayoutElement> Tile<W> {
size.h = max(1, size.h - width * 2);
}
self.window.request_size(size);
self.window.request_size(size, animate);
}
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
@@ -228,35 +493,226 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn has_ssd(&self) -> bool {
self.effective_border_width().is_some() || self.window.has_ssd()
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
}
self.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !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>>
view_size: Size<i32, Logical>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
let alpha = if self.is_fullscreen {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
let window_loc = self.window_loc();
let window_size = self.window_size();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
let gles_renderer = renderer.as_gles_renderer();
// If we're resizing, try to render a crossfade, or a fallback.
let mut crossfade = None;
let mut crossfade_fallback = None;
if let Some(resize) = &self.resize_animation {
if Shaders::get(gles_renderer).crossfade.is_some() {
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
let window_elements =
self.window
.render(gles_renderer, Point::from((0, 0)), scale, 1., target);
let current = render_to_encompassing_texture(
gles_renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&window_elements,
)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
let elem = CrossfadeRenderElement::new(
gles_renderer,
area,
scale,
texture_from.clone(),
resize.snapshot.size,
(texture_current, texture_current_geo),
window_size,
resize.anim.clamped_value().clamp(0., 1.) as f32,
alpha,
)
.expect("we checked the crossfade shader above");
self.window
.set_offscreen_element_id(Some(elem.id().clone()));
crossfade = Some(elem.into());
}
}
}
if crossfade.is_none() {
let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
crossfade_fallback = Some(
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
);
self.window.set_offscreen_element_id(None);
}
}
// If we're not resizing, render the window itself.
let mut window = None;
if crossfade.is_none() && crossfade_fallback.is_none() {
window = Some(
self.window
.render(renderer, window_render_loc, scale, alpha, target)
.into_iter()
.map(Into::into),
);
}
let rv = crossfade
.into_iter()
.chain(crossfade_fallback)
.chain(window.into_iter().flatten());
let elem = self.effective_border_width().map(|width| {
self.border
.render(
renderer,
location + Point::from((width, width)),
scale,
view_size,
)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| {
self.focus_ring
.render(renderer, location, scale, view_size)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = self.is_fullscreen.then(|| {
SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
rv.chain(elem)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
if let Some(anim) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements =
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
renderer,
scale.x as i32,
&elements,
anim.clamped_value().clamp(0., 1.) as f32,
);
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
let mut center = location;
center.x += self.tile_size().w / 2;
center.y += self.tile_size().h / 2;
Some(TileRenderElement::Offscreen(
RescaleRenderElement::from_element(
elem,
center.to_physical_precise_round(scale),
(anim.value() / 2. + 0.5).max(0.),
),
))
.into_iter()
.chain(None.into_iter().flatten())
} else {
self.window().set_offscreen_element_id(None);
let elements =
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
fn render_snapshot<E, C>(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
contents: Vec<C>,
) -> Vec<TileSnapshotRenderElement>
where
<R as Renderer>::TextureId: 'static,
E: Into<TileSnapshotContentsRenderElement>,
C: ToRenderElement<RenderElement = E>,
{
let mut rv = Vec::new();
let alpha = if self.is_fullscreen {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
let window_pos = location + self.window_loc();
rv.extend(self.window.render(renderer, window_pos, scale));
let window_size = self.window_size();
let animated_window_size = self.animated_window_size();
let animated_scale = animated_window_size.to_f64() / window_size.to_f64();
if self.effective_border_width().is_some() {
let mut rv = vec![];
for baked in contents {
let elem = baked.to_render_element(self.window_loc(), scale, alpha, Kind::Unspecified);
let elem: TileSnapshotContentsRenderElement = elem.into();
let origin = self.window_loc().to_physical_precise_round(scale);
let elem = RescaleRenderElement::from_element(elem, origin, animated_scale);
rv.push(elem.into());
}
if let Some(width) = self.effective_border_width() {
rv.extend(
self.border
.render(scale)
.map(|elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
})
.render(renderer, Point::from((width, width)), scale, view_size)
.map(Into::into),
);
}
@@ -264,7 +720,7 @@ impl<W: LayoutElement> Tile<W> {
if self.is_fullscreen {
let elem = SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
Point::from((0, 0)),
scale,
1.,
Kind::Unspecified,
@@ -274,4 +730,27 @@ impl<W: LayoutElement> Tile<W> {
rv
}
pub fn take_snapshot_for_close_anim(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> Option<RenderSnapshot<TileSnapshotRenderElement, TileSnapshotRenderElement>> {
let snapshot = self.window.take_unmap_snapshot()?;
Some(RenderSnapshot {
contents: self.render_snapshot(renderer, scale, view_size, snapshot.contents),
blocked_out_contents: self.render_snapshot(
renderer,
scale,
view_size,
snapshot.blocked_out_contents,
),
block_out_from: snapshot.block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
blocked_out_texture: Default::default(),
})
}
}
+1448 -286
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
#[macro_use]
extern crate tracing;
pub mod animation;
pub mod backend;
pub mod cli;
pub mod cursor;
#[cfg(feature = "dbus")]
pub mod dbus;
pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layout;
pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod rubber_band;
pub mod scroll_tracker;
pub mod swipe_tracker;
pub mod ui;
pub mod utils;
pub mod window;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
+137 -108
View File
@@ -1,95 +1,33 @@
#[macro_use]
extern crate tracing;
mod animation;
mod backend;
mod config_error_notification;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod exit_confirm_dialog;
mod frame_clock;
mod handlers;
mod hotkey_overlay;
mod input;
mod ipc;
mod layout;
mod niri;
mod render_helpers;
mod screenshot_ui;
mod utils;
mod watcher;
#[cfg(not(feature = "xdp-gnome-screencast"))]
mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
mod pw_utils;
use std::ffi::OsString;
use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
use clap::Parser;
use directories::ProjectDirs;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
use niri::{Niri, State};
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::{self, EventLoop};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
use utils::spawn;
use watcher::Watcher;
use crate::ipc::client::handle_msg;
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
/// Command to run upon compositor startup.
#[arg(last = true)]
command: Vec<OsString>,
#[command(subcommand)]
subcommand: Option<Sub>,
}
#[derive(Subcommand)]
enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
enum Msg {
/// List connected outputs.
Outputs,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
@@ -102,7 +40,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let is_systemd_service = env::var_os("NOTIFY_SOCKET").is_some();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
#[cfg(not(feature = "systemd"))]
warn!(
"running as a systemd service, but systemd support is compiled out. \
Are you sure you did not forget to set `--features systemd`?"
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
@@ -111,21 +57,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_env_filter(env_filter)
.init();
if is_systemd_service {
// If we're starting as a systemd service, assume that the intention is to start on a TTY.
// Remove DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will
// cause the winit backend to be selected instead.
let cli = Cli::parse();
if cli.session {
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
// the winit backend to be selected instead.
if env::var_os("DISPLAY").is_some() {
debug!("we're running as a systemd service but DISPLAY is set, removing it");
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
}
if env::var_os("WAYLAND_DISPLAY").is_some() {
debug!("we're running as a systemd service but WAYLAND_DISPLAY is set, removing it");
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
}
let cli = Cli::parse();
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
// Ensure the session type is set to Wayland for xdg-autostart and Qt apps.
env::set_var("XDG_SESSION_TYPE", "wayland");
}
let _client = tracy_client::Client::start();
@@ -154,7 +105,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("starting version {}", &version());
// Load the config.
let path = cli.config.or_else(default_config_path);
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
}
Some(default_path)
});
let mut config_errored = false;
let mut config = path
@@ -169,8 +157,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
})
.unwrap_or_default();
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
// Create the compositor.
let mut event_loop = EventLoop::try_new().unwrap();
@@ -180,7 +175,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
);
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
@@ -196,9 +192,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
if is_systemd_service {
// We're starting as a systemd service. Export our variables.
import_env_to_systemd();
if cli.session {
// We're starting as a session. Import our variables.
import_environment();
// Inhibit power key handling so we can suspend on it.
#[cfg(feature = "dbus")]
@@ -210,15 +206,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
#[cfg(feature = "dbus")]
dbus::DBusServers::start(&mut state, is_systemd_service);
dbus::DBusServers::start(&mut state, cli.session);
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
// Send ready notification to the NOTIFY_FD file descriptor.
if let Err(err) = notify_fd() {
warn!("error notifying fd: {err:?}");
}
// Set up config file watcher.
let _watcher = if let Some(path) = path {
let _watcher = if let Some(path) = path.clone() {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
event_loop
@@ -243,6 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Show the config error notification right away if needed.
if config_errored {
state.niri.config_error_notification.show();
} else if config_created {
state.niri.config_error_notification.show_created(path);
}
// Run the compositor.
@@ -253,21 +256,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
fn import_environment() {
let variables = [
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
]
.join(" ");
let mut init_system_import = String::new();
if cfg!(feature = "systemd") {
write!(
init_system_import,
"systemctl --user import-environment {variables};"
)
.unwrap();
}
if cfg!(feature = "dinit") {
write!(init_system_import, "dinitctl setenv {variables};").unwrap();
}
fn import_env_to_systemd() {
let rv = Command::new("/bin/sh")
.args([
"-c",
"systemctl --user import-environment WAYLAND_DISPLAY && \
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment WAYLAND_DISPLAY",
&format!(
"{init_system_import}\
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment {variables}"
),
])
.spawn();
// Wait for the import process to complete, otherwise services will start too fast without
@@ -284,7 +301,7 @@ fn import_env_to_systemd() {
}
},
Err(err) => {
warn!("error spawning shell to import environment into systemd: {err:?}");
warn!("error spawning shell to import environment: {err:?}");
}
}
}
@@ -299,3 +316,15 @@ fn default_config_path() -> Option<PathBuf> {
path.push("config.kdl");
Some(path)
}
fn notify_fd() -> anyhow::Result<()> {
let fd = match env::var("NOTIFY_FD") {
Ok(notify_fd) => notify_fd.parse()?,
Err(env::VarError::NotPresent) => return Ok(()),
Err(err) => return Err(err.into()),
};
env::remove_var("NOTIFY_FD");
let mut notif = unsafe { File::from_raw_fd(fd) };
notif.write_all(b"READY=1\n")?;
Ok(())
}
+1158 -682
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(|mapped, output| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
});
}
}
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
let Some(client) = wl_output.client() else {
return;
};
let protocol_state = &mut state.niri.foreign_toplevel_state;
for data in protocol_state.toplevels.values_mut() {
if data.output.as_ref() != Some(output) {
continue;
}
for (instance, outputs) in &mut data.instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
instance.output_enter(wl_output);
instance.done();
outputs.push(wl_output.clone());
}
}
}
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
role: &XdgToplevelSurfaceRoleAttributes,
output: Option<&Output>,
has_focus: bool,
) {
let states = to_state_vec(&role.current.states, has_focus);
match protocol_state.toplevels.entry(wl_surface.clone()) {
Entry::Occupied(entry) => {
// Existing window, check if anything changed.
let data = entry.into_mut();
let mut new_title = None;
if data.title != role.title {
data.title.clone_from(&role.title);
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.clone_from(&role.app_id);
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);
};
}
+246
View File
@@ -0,0 +1,246 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use wayland_protocols_wlr::gamma_control::v1::server::{
zwlr_gamma_control_manager_v1, zwlr_gamma_control_v1,
};
use zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1;
use zwlr_gamma_control_v1::ZwlrGammaControlV1;
const VERSION: u32 = 1;
pub struct GammaControlManagerState {
// Active gamma controls only. Failed ones are removed.
gamma_controls: HashMap<Output, ZwlrGammaControlV1>,
}
pub struct GammaControlManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait GammaControlHandler {
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState;
fn get_gamma_size(&mut self, output: &Output) -> Option<u32>;
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()>;
}
pub struct GammaControlState {
gamma_size: u32,
}
impl GammaControlManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = GammaControlManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrGammaControlManagerV1, _>(VERSION, global_data);
Self {
gamma_controls: HashMap::new(),
}
}
pub fn output_removed(&mut self, output: &Output) {
if let Some(gamma_control) = self.gamma_controls.remove(output) {
gamma_control.failed();
}
}
}
impl<D> GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData, D>
for GammaControlManagerState
where
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<ZwlrGammaControlManagerV1>,
_manager_state: &GammaControlManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &GammaControlManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrGammaControlManagerV1, (), D> for GammaControlManagerState
where
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
_resource: &ZwlrGammaControlManagerV1,
request: <ZwlrGammaControlManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_gamma_control_manager_v1::Request::GetGammaControl { id, output } => {
if let Some(output) = Output::from_resource(&output) {
// We borrow state in the middle.
#[allow(clippy::map_entry)]
if !state
.gamma_control_manager_state()
.gamma_controls
.contains_key(&output)
{
if let Some(gamma_size) = state.get_gamma_size(&output) {
let zwlr_gamma_control =
data_init.init(id, GammaControlState { gamma_size });
zwlr_gamma_control.gamma_size(gamma_size);
state
.gamma_control_manager_state()
.gamma_controls
.insert(output, zwlr_gamma_control);
return;
}
}
}
data_init
.init(id, GammaControlState { gamma_size: 0 })
.failed();
}
zwlr_gamma_control_manager_v1::Request::Destroy => (),
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrGammaControlV1, GammaControlState, D> for GammaControlManagerState
where
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrGammaControlV1,
request: <ZwlrGammaControlV1 as Resource>::Request,
data: &GammaControlState,
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_gamma_control_v1::Request::SetGamma { fd } => {
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
return;
};
let output = output.clone();
trace!("setting gamma for output {}", output.name());
// Start with a u16 slice so it's aligned correctly.
let mut gamma = vec![0u16; data.gamma_size as usize * 3];
let buf = bytemuck::cast_slice_mut(&mut gamma);
let mut file = File::from(fd);
{
let _span = tracy_client::span!("read gamma from fd");
if let Err(err) = file.read_exact(buf) {
warn!("failed to read gamma data: {err:?}");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
// Verify that there's no more data.
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
{
match file.read(&mut [0]) {
Ok(0) => (),
Ok(_) => {
warn!("gamma data is too large");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
Err(err) => {
warn!("error reading gamma data: {err:?}");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
}
}
}
if state.set_gamma(&output, Some(gamma)).is_none() {
resource.failed();
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
}
}
zwlr_gamma_control_v1::Request::Destroy => (),
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrGammaControlV1,
_data: &GammaControlState,
) {
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
return;
};
let output = output.clone();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
}
}
#[macro_export]
macro_rules! delegate_gamma_control {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: $crate::protocols::gamma_control::GammaControlManagerGlobalData
] => $crate::protocols::gamma_control::GammaControlManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: ()
] => $crate::protocols::gamma_control::GammaControlManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_v1::ZwlrGammaControlV1: $crate::protocols::gamma_control::GammaControlState
] => $crate::protocols::gamma_control::GammaControlManagerState);
};
}
+3
View File
@@ -0,0 +1,3 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod screencopy;
+386
View File
@@ -0,0 +1,386 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::wayland::shm;
// We do not support copy_with_damage() semantics yet.
const VERSION: u32 = 1;
pub struct ScreencopyManagerState;
pub struct ScreencopyManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ScreencopyManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ScreencopyManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
Self
}
}
impl<D> GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData, D>
for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_display: &DisplayHandle,
_client: &Client,
manager: New<ZwlrScreencopyManagerV1>,
_manager_state: &ScreencopyManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrScreencopyManagerV1, (), D> for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_manager: &ZwlrScreencopyManagerV1,
request: zwlr_screencopy_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let (frame, overlay_cursor, buffer_size, region_loc, output) = match request {
zwlr_screencopy_manager_v1::Request::CaptureOutput {
frame,
overlay_cursor,
output,
} => {
let output = Output::from_resource(&output).unwrap();
let buffer_size = output.current_mode().unwrap().size;
let region_loc = Point::from((0, 0));
(frame, overlay_cursor, buffer_size, region_loc, output)
}
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
frame,
overlay_cursor,
x,
y,
width,
height,
output,
} => {
if width <= 0 || height <= 0 {
trace!("screencopy client requested invalid sized region");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
}
let output = Output::from_resource(&output).unwrap();
let output_transform = output.current_transform();
let output_physical_size =
output_transform.transform_size(output.current_mode().unwrap().size);
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let output_scale = output.current_scale().integer_scale();
let physical_rect = rect.to_physical(output_scale);
// Clamp captured region to the output.
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
trace!("screencopy client requested region outside of output");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
};
let untransformed_rect = output_transform
.invert()
.transform_rect_in(clamped_rect, &output_physical_size);
(
frame,
overlay_cursor,
untransformed_rect.size,
clamped_rect.loc,
output,
)
}
zwlr_screencopy_manager_v1::Request::Destroy => return,
_ => unreachable!(),
};
// Create the frame.
let overlay_cursor = overlay_cursor != 0;
let info = ScreencopyFrameInfo {
output,
overlay_cursor,
buffer_size,
region_loc,
};
let frame = data_init.init(
frame,
ScreencopyFrameState::Pending {
info,
copied: Arc::new(AtomicBool::new(false)),
},
);
// Send desired SHM buffer parameters.
frame.buffer(
wl_shm::Format::Argb8888,
buffer_size.w as u32,
buffer_size.h as u32,
buffer_size.w as u32 * 4,
);
// if manager.version() >= 3 {
// // Send desired DMA buffer parameters.
// frame.linux_dmabuf(
// Fourcc::Argb8888 as u32,
// buffer_size.w as u32,
// buffer_size.h as u32,
// );
//
// // Notify client that all supported buffers were enumerated.
// frame.buffer_done();
// }
}
}
/// Handler trait for wlr-screencopy.
pub trait ScreencopyHandler {
/// Handle new screencopy request.
fn frame(&mut self, frame: Screencopy);
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! delegate_screencopy {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: $crate::protocols::screencopy::ScreencopyManagerGlobalData
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: ()
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::protocols::screencopy::ScreencopyFrameState
] => $crate::protocols::screencopy::ScreencopyManagerState);
};
}
#[derive(Clone)]
pub struct ScreencopyFrameInfo {
output: Output,
buffer_size: Size<i32, Physical>,
region_loc: Point<i32, Physical>,
overlay_cursor: bool,
}
pub enum ScreencopyFrameState {
Failed,
Pending {
info: ScreencopyFrameInfo,
copied: Arc<AtomicBool>,
},
}
impl<D> Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState, D> for ScreencopyManagerState
where
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
frame: &ZwlrScreencopyFrameV1,
request: zwlr_screencopy_frame_v1::Request,
data: &ScreencopyFrameState,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
if matches!(request, zwlr_screencopy_frame_v1::Request::Destroy) {
return;
}
let (info, copied) = match data {
ScreencopyFrameState::Failed => return,
ScreencopyFrameState::Pending { info, copied } => (info, copied),
};
if copied.load(Ordering::SeqCst) {
frame.post_error(
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
"copy was already requested",
);
return;
}
let (buffer, with_damage) = match request {
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
_ => unreachable!(),
};
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == info.buffer_size.w * 4
&& buffer_data.height == info.buffer_size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height
})
.unwrap_or(false)
{
frame.post_error(
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
"invalid buffer",
);
return;
}
copied.store(true, Ordering::SeqCst);
state.frame(Screencopy {
with_damage,
buffer,
frame: frame.clone(),
info: info.clone(),
submitted: false,
});
}
}
/// Screencopy frame.
pub struct Screencopy {
info: ScreencopyFrameInfo,
frame: ZwlrScreencopyFrameV1,
#[allow(unused)]
with_damage: bool,
buffer: WlBuffer,
submitted: bool,
}
impl Drop for Screencopy {
fn drop(&mut self) {
if !self.submitted {
self.frame.failed();
}
}
}
impl Screencopy {
/// Get the target buffer to copy to.
pub fn buffer(&self) -> &WlBuffer {
&self.buffer
}
pub fn region_loc(&self) -> Point<i32, Physical> {
self.info.region_loc
}
pub fn buffer_size(&self) -> Size<i32, Physical> {
self.info.buffer_size
}
pub fn output(&self) -> &Output {
&self.info.output
}
pub fn overlay_cursor(&self) -> bool {
self.info.overlay_cursor
}
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
// assert!(self.with_damage);
//
// for Rectangle { loc, size } in damage {
// self.frame
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
// }
// }
/// Submit the copied content.
pub fn submit(mut self, y_invert: bool) {
// Notify client that buffer is ordinary.
self.frame.flags(if y_invert {
Flags::YInvert
} else {
Flags::empty()
});
// Notify client about successful copy.
let time = UNIX_EPOCH.elapsed().unwrap();
let tv_sec_hi = (time.as_secs() >> 32) as u32;
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
let tv_nsec = time.subsec_nanos();
self.frame.ready(tv_sec_hi, tv_sec_lo, tv_nsec);
// Mark frame as submitted to ensure destructor isn't run.
self.submitted = true;
}
// pub fn submit_after_sync<T>(
// self,
// y_invert: bool,
// sync_point: Option<OwnedFd>,
// event_loop: &LoopHandle<'_, T>,
// ) {
// match sync_point {
// None => self.submit(y_invert),
// Some(sync_fd) => {
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
// let mut screencopy = Some(self);
// event_loop
// .insert_source(source, move |_, _, _| {
// screencopy.take().unwrap().submit(y_invert);
// Ok(PostAction::Remove)
// })
// .unwrap();
// }
// }
// }
}
+25 -19
View File
@@ -7,42 +7,48 @@ use std::rc::Rc;
use std::time::Duration;
use anyhow::Context as _;
use pipewire::spa::data::DataType;
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::context::Context;
use pipewire::core::Core;
use pipewire::main_loop::MainLoop;
use pipewire::properties::Properties;
use pipewire::spa::buffer::DataType;
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::spa::param::format_utils::parse_format;
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
use pipewire::spa::param::ParamType;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
use pipewire::spa::sys::*;
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
use pipewire::spa::Direction;
use pipewire::spa::utils::{
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
};
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
use pipewire::{Context, Core, MainLoop, Properties};
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::DrmDeviceFd;
use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
use crate::niri::State;
pub struct PipeWire {
_context: Context<MainLoop>,
_context: Context,
pub core: Core,
}
pub struct Cast {
pub session_id: usize,
pub stream: Rc<Stream>,
pub stream: Stream,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub size: Size<i32, Physical>,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
@@ -51,7 +57,7 @@ pub struct Cast {
impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new().context("error creating MainLoop")?;
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
@@ -66,14 +72,14 @@ impl PipeWire {
struct AsFdWrapper(MainLoop);
impl AsFd for AsFdWrapper {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.fd()
self.0.loop_().fd()
}
}
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
event_loop
.insert_source(generic, move |_, wrapper, _| {
let _span = tracy_client::span!("pipewire iteration");
wrapper.0.iterate(Duration::ZERO);
wrapper.0.loop_().iterate(Duration::ZERO);
Ok(PostAction::Continue)
})
.unwrap();
@@ -112,13 +118,14 @@ impl PipeWire {
let mode = output.current_mode().unwrap();
let size = mode.size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
.context("error creating Stream")?;
// Like in good old wayland-rs times...
let stream = Rc::new(stream);
let node_id = Rc::new(Cell::new(None));
let is_active = Rc::new(Cell::new(false));
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
@@ -127,10 +134,9 @@ impl PipeWire {
let listener = stream
.add_local_listener_with_user_data(())
.state_changed({
let stream = stream.clone();
let is_active = is_active.clone();
let stop_cast = stop_cast.clone();
move |old, new| {
move |stream, (), old, new| {
debug!("pw stream: state changed: {old:?} -> {new:?}");
match new {
@@ -174,7 +180,7 @@ impl PipeWire {
})
.param_changed({
let min_time_between_frames = min_time_between_frames.clone();
move |stream, id, _data, pod| {
move |stream, (), id, pod| {
let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed");
@@ -256,8 +262,7 @@ impl PipeWire {
let mut b1 = vec![];
// let mut b2 = vec![];
let mut params = [
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
];
stream.update_params(&mut params).unwrap();
}
@@ -265,7 +270,7 @@ impl PipeWire {
.add_buffer({
let dmabufs = dmabufs.clone();
let stop_cast = stop_cast.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: add_buffer");
unsafe {
@@ -309,7 +314,7 @@ impl PipeWire {
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: remove_buffer");
unsafe {
@@ -383,6 +388,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+162
View File
@@ -0,0 +1,162 @@
use std::collections::HashMap;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::primary_gpu_pixel_shader_with_textures::PrimaryGpuPixelShaderWithTexturesRenderElement;
use super::renderer::AsGlesFrame;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
#[derive(Debug)]
pub struct CrossfadeRenderElement(PrimaryGpuPixelShaderWithTexturesRenderElement);
impl CrossfadeRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
renderer: &mut GlesRenderer,
area: Rectangle<i32, Logical>,
scale: Scale<f64>,
texture_from: (GlesTexture, Rectangle<i32, Physical>),
size_from: Size<i32, Logical>,
texture_to: (GlesTexture, Rectangle<i32, Physical>),
size_to: Size<i32, Logical>,
amount: f32,
result_alpha: f32,
) -> Option<Self> {
let (texture_from, texture_from_geo) = texture_from;
let (texture_to, texture_to_geo) = texture_to;
let scale_from = area.size.to_f64() / size_from.to_f64();
let scale_to = area.size.to_f64() / size_to.to_f64();
let tex_from_geo = texture_from_geo.to_f64().upscale(scale_from);
let tex_to_geo = texture_to_geo.to_f64().upscale(scale_to);
let combined_geo = tex_from_geo.merge(tex_to_geo);
let size = combined_geo
.size
.to_logical(1.)
.to_buffer(1., Transform::Normal);
let area = Rectangle::from_loc_and_size(
area.loc + combined_geo.loc.to_logical(scale).to_i32_round(),
combined_geo.size.to_logical(scale).to_i32_round(),
);
let tex_from_loc = (tex_from_geo.loc - combined_geo.loc)
.downscale((combined_geo.size.w, combined_geo.size.h));
let tex_to_loc = (tex_to_geo.loc - combined_geo.loc)
.downscale((combined_geo.size.w, combined_geo.size.h));
let tex_from_size = tex_from_geo.size / combined_geo.size;
let tex_to_size = tex_to_geo.size / combined_geo.size;
// FIXME: cropping this element will mess up the coordinates.
Shaders::get(renderer).crossfade.clone().map(|shader| {
Self(PrimaryGpuPixelShaderWithTexturesRenderElement::new(
shader,
HashMap::from([
(String::from("tex_from"), texture_from),
(String::from("tex_to"), texture_to),
]),
area,
size,
None,
result_alpha,
vec![
Uniform::new(
"tex_from_loc",
(tex_from_loc.x as f32, tex_from_loc.y as f32),
),
Uniform::new(
"tex_from_size",
(tex_from_size.x as f32, tex_from_size.y as f32),
),
Uniform::new("tex_to_loc", (tex_to_loc.x as f32, tex_to_loc.y as f32)),
Uniform::new("tex_to_size", (tex_to_size.x as f32, tex_to_size.y as f32)),
Uniform::new("amount", amount),
],
Kind::Unspecified,
))
})
}
}
impl Element for CrossfadeRenderElement {
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>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for CrossfadeRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for CrossfadeRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+135
View File
@@ -0,0 +1,135 @@
use glam::Vec2;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
use super::renderer::NiriRenderer;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
#[derive(Debug)]
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
impl GradientRenderElement {
pub fn new(
renderer: &mut impl NiriRenderer,
scale: Scale<f64>,
area: Rectangle<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
) -> Option<Self> {
let shader = Shaders::get(renderer).gradient_border.clone()?;
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
let grad_dir = Vec2::from_angle(angle);
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
let mut grad_area_diag = Vec2::new(w, h);
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
grad_area_diag.x = -w;
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y <= 0. {
grad_vec = -grad_vec;
}
let elem = PixelShaderElement::new(
shader,
area,
None,
1.,
vec![
Uniform::new("color_from", color_from),
Uniform::new("color_to", color_to),
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
],
Kind::Unspecified,
);
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
}
}
impl Element for GradientRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for GradientRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+276
View File
@@ -0,0 +1,276 @@
use std::ptr;
use anyhow::{ensure, Context};
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::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::shm;
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
pub mod crossfade;
pub mod gradient;
pub mod offscreen;
pub mod primary_gpu_pixel_shader;
pub mod primary_gpu_pixel_shader_with_textures;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
pub mod resources;
pub mod shaders;
pub mod snapshot;
pub mod surface;
/// What we're rendering for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTarget {
/// Rendering to display on screen.
Output,
/// Rendering for a screencast.
Screencast,
/// Rendering for any other screen capture.
ScreenCapture,
}
/// Buffer with location, src and dst.
#[derive(Debug)]
pub struct BakedBuffer<B> {
pub buffer: B,
pub location: Point<i32, Logical>,
pub src: Option<Rectangle<f64, Logical>>,
pub dst: Option<Size<i32, Logical>>,
}
pub trait ToRenderElement {
type RenderElement;
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement;
}
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
type RenderElement = PrimaryGpuTextureRenderElement;
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
let elem = TextureRenderElement::from_texture_buffer(
(location + self.location).to_physical_precise_round(scale),
&self.buffer,
Some(alpha),
self.src,
self.dst,
kind,
);
PrimaryGpuTextureRenderElement(elem)
}
}
impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
type RenderElement = SolidColorRenderElement;
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
SolidColorRenderElement::from_buffer(
&self.buffer,
(location + self.location)
.to_physical_precise_round(scale)
.to_i32_round(),
scale,
alpha,
kind,
)
}
}
pub fn render_to_encompassing_texture(
renderer: &mut GlesRenderer,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: &[impl RenderElement<GlesRenderer>],
) -> anyhow::Result<(GlesTexture, SyncPoint, Rectangle<i32, Physical>)> {
let geo = elements
.iter()
.map(|ele| ele.geometry(scale))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
});
let (texture, sync_point) =
render_to_texture(renderer, geo.size, scale, transform, fourcc, elements)?;
Ok((texture, sync_point, geo))
}
pub fn render_to_texture(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
let _span = tracy_client::span!();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
renderer
.bind(texture.clone())
.context("error binding texture")?;
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
Ok((texture, sync_point))
}
pub fn render_and_download(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!();
let (_, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
pub fn render_to_vec(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<Vec<u8>> {
let _span = tracy_client::span!();
let mapping = render_and_download(renderer, size, scale, transform, fourcc, elements)
.context("error rendering")?;
let copy = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
Ok(copy.to_vec())
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let _span = tracy_client::span!();
renderer.bind(dmabuf).context("error binding texture")?;
render_elements(renderer, size, scale, transform, elements)
}
pub fn render_to_shm(
renderer: &mut GlesRenderer,
buffer: &WlBuffer,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
ensure!(
// The buffer prefers pixels in little endian ...
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == size.w * 4
&& buffer_data.height == size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
"invalid buffer format or size"
);
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
unsafe {
let _span = tracy_client::span!("copy_nonoverlapping");
ptr::copy_nonoverlapping(bytes.as_ptr(), shm_buffer.cast(), shm_len);
}
Ok(())
})
.context("expected shm buffer, but didn't get one")?
}
fn render_elements(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let transform = transform.invert();
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
.context("error starting frame")?;
frame
.clear([0., 0., 0., 0.], &[output_rect])
.context("error clearing")?;
for element in elements {
let src = element.src();
let dst = element.geometry(scale);
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
frame.finish().context("error finishing frame")
}
+216
View File
@@ -0,0 +1,216 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders elements into an off-screen buffer.
#[derive(Debug)]
pub struct OffscreenRenderElement {
// The texture, if rendering succeeded.
texture: Option<PrimaryGpuTextureRenderElement>,
// The fallback buffer in case the rendering fails.
fallback: SolidColorRenderElement,
}
impl OffscreenRenderElement {
pub fn new(
renderer: &mut GlesRenderer,
scale: i32,
elements: &[impl RenderElement<GlesRenderer>],
result_alpha: f32,
) -> Self {
let _span = tracy_client::span!("OffscreenRenderElement::new");
let geo = elements
.iter()
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let logical_size = geo.size.to_logical(scale);
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
let fallback = SolidColorRenderElement::from_buffer(
&fallback_buffer,
geo.loc,
Scale::from(scale as f64),
result_alpha,
Kind::Unspecified,
);
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
});
match render_to_texture(
renderer,
geo.size,
Scale::from(scale as f64),
Transform::Normal,
Fourcc::Abgr8888,
elements,
) {
Ok((texture, _sync_point)) => {
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let element = TextureRenderElement::from_texture_buffer(
geo.loc.to_f64(),
&buffer,
Some(result_alpha),
None,
None,
Kind::Unspecified,
);
Self {
texture: Some(PrimaryGpuTextureRenderElement(element)),
fallback,
}
}
Err(err) => {
warn!("error off-screening elements: {err:?}");
Self {
texture: None,
fallback,
}
}
}
}
}
impl Element for OffscreenRenderElement {
fn id(&self) -> &Id {
if let Some(texture) = &self.texture {
texture.id()
} else {
self.fallback.id()
}
}
fn current_commit(&self) -> CommitCounter {
if let Some(texture) = &self.texture {
texture.current_commit()
} else {
self.fallback.current_commit()
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
if let Some(texture) = &self.texture {
texture.geometry(scale)
} else {
self.fallback.geometry(scale)
}
}
fn transform(&self) -> Transform {
if let Some(texture) = &self.texture {
texture.transform()
} else {
self.fallback.transform()
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
if let Some(texture) = &self.texture {
texture.src()
} else {
self.fallback.src()
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if let Some(texture) = &self.texture {
texture.damage_since(scale, commit)
} else {
self.fallback.damage_since(scale, commit)
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
if let Some(texture) = &self.texture {
texture.opaque_regions(scale)
} else {
self.fallback.opaque_regions(scale)
}
}
fn alpha(&self) -> f32 {
if let Some(texture) = &self.texture {
texture.alpha()
} else {
self.fallback.alpha()
}
}
fn kind(&self) -> Kind {
if let Some(texture) = &self.texture {
texture.kind()
} else {
self.fallback.kind()
}
}
}
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
@@ -0,0 +1,97 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
impl Element for PrimaryGpuPixelShaderRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
@@ -0,0 +1,430 @@
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::rc::Rc;
use glam::{Mat3, Vec2};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{
ffi, link_program, Capability, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform,
UniformDesc, UniformName,
};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size};
use super::renderer::AsGlesFrame;
use super::resources::Resources;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU.
///
/// The shader accepts textures as input.
#[derive(Debug)]
pub struct PrimaryGpuPixelShaderWithTexturesRenderElement {
shader: PixelWithTexturesProgram,
textures: HashMap<String, GlesTexture>,
id: Id,
commit_counter: CommitCounter,
area: Rectangle<i32, Logical>,
size: Size<f64, Buffer>,
opaque_regions: Vec<Rectangle<i32, Logical>>,
alpha: f32,
additional_uniforms: Vec<Uniform<'static>>,
kind: Kind,
}
#[derive(Debug, Clone)]
pub struct PixelWithTexturesProgram(Rc<PixelWithTexturesProgramInner>);
#[derive(Debug)]
struct PixelWithTexturesProgramInner {
program: ffi::types::GLuint,
uniform_tex_matrix: ffi::types::GLint,
uniform_matrix: ffi::types::GLint,
uniform_size: ffi::types::GLint,
uniform_alpha: ffi::types::GLint,
attrib_vert: ffi::types::GLint,
attrib_vert_position: ffi::types::GLint,
additional_uniforms: HashMap<String, UniformDesc>,
texture_uniforms: HashMap<String, ffi::types::GLint>,
}
unsafe fn compile_program(
gl: &ffi::Gles2,
src: &str,
additional_uniforms: &[UniformName<'_>],
texture_uniforms: &[&str],
// destruction_callback_sender: Sender<CleanupResource>,
) -> Result<PixelWithTexturesProgram, GlesError> {
let shader = src;
let program = unsafe { link_program(gl, include_str!("shaders/texture.vert"), shader)? };
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
let size = CStr::from_bytes_with_nul(b"size\0").expect("NULL terminated");
let alpha = CStr::from_bytes_with_nul(b"alpha\0").expect("NULL terminated");
Ok(PixelWithTexturesProgram(Rc::new(
PixelWithTexturesProgramInner {
program,
uniform_matrix: gl
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
uniform_size: gl
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
uniform_alpha: gl
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
additional_uniforms: additional_uniforms
.iter()
.map(|uniform| {
let name =
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
(
uniform.name.clone().into_owned(),
UniformDesc {
location,
type_: uniform.type_,
},
)
})
.collect(),
texture_uniforms: texture_uniforms
.iter()
.map(|name_| {
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
(name_.to_string(), location)
})
.collect(),
},
)))
}
impl PixelWithTexturesProgram {
pub fn compile(
renderer: &mut GlesRenderer,
src: &str,
additional_uniforms: &[UniformName<'_>],
texture_uniforms: &[&str],
) -> Result<Self, GlesError> {
renderer.with_context(move |gl| unsafe {
compile_program(gl, src, additional_uniforms, texture_uniforms)
})?
}
}
impl PrimaryGpuPixelShaderWithTexturesRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
shader: PixelWithTexturesProgram,
textures: HashMap<String, GlesTexture>,
area: Rectangle<i32, Logical>,
size: Size<f64, Buffer>,
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
alpha: f32,
additional_uniforms: Vec<Uniform<'_>>,
kind: Kind,
) -> Self {
Self {
shader,
textures,
id: Id::new(),
commit_counter: CommitCounter::default(),
area,
size,
opaque_regions: opaque_regions.unwrap_or_default(),
alpha,
additional_uniforms: additional_uniforms
.into_iter()
.map(|u| u.into_owned())
.collect(),
kind,
}
}
}
impl Element for PrimaryGpuPixelShaderWithTexturesRenderElement {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit_counter
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), self.size.to_f64())
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.area.to_physical_precise_round(scale)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.opaque_regions
.iter()
.map(|region| region.to_physical_precise_round(scale))
.collect()
}
fn alpha(&self) -> f32 {
1.0
}
fn kind(&self) -> Kind {
self.kind
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderWithTexturesRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let frame = frame.as_gles_frame();
let Some(resources) = Resources::get(frame) else {
return Ok(());
};
let mut resources = resources.borrow_mut();
let supports_instancing = frame.capabilities().contains(&Capability::Instancing);
// prepare the vertices
resources.vertices.clear();
if supports_instancing {
resources.vertices.extend(damage.iter().flat_map(|rect| {
let dest_size = dest.size;
let rect_constrained_loc = rect
.loc
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
let rect_clamped_size = rect.size.clamp(
(0, 0),
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
[
rect.loc.x as f32,
rect.loc.y as f32,
rect.size.w as f32,
rect.size.h as f32,
]
}));
} else {
resources.vertices.extend(damage.iter().flat_map(|rect| {
let dest_size = dest.size;
let rect_constrained_loc = rect
.loc
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
let rect_clamped_size = rect.size.clamp(
(0, 0),
(dest_size.to_point() - rect_constrained_loc).to_size(),
);
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
(0..6).flat_map(move |_| {
[
rect.loc.x as f32,
rect.loc.y as f32,
rect.size.w as f32,
rect.size.h as f32,
]
})
}));
}
if resources.vertices.is_empty() {
return Ok(());
}
// dest position and scale
let mut matrix = Mat3::from_translation(Vec2::new(dest.loc.x as f32, dest.loc.y as f32));
let scale = src.size.to_f64() / dest.size.to_f64();
let tex_matrix = Mat3::from_scale(Vec2::new(scale.x as f32, scale.y as f32));
let tex_matrix =
Mat3::from_translation(Vec2::new(src.loc.x as f32, src.loc.y as f32)) * tex_matrix;
let tex_matrix = Mat3::from_scale(Vec2::new(
(1.0f64 / self.size.w) as f32,
(1.0f64 / self.size.h) as f32,
)) * tex_matrix;
//apply output transformation
matrix = Mat3::from_cols_array(frame.projection()) * matrix;
let program = &self.shader.0;
// render
frame.with_context(move |gl| -> Result<(), GlesError> {
unsafe {
for (i, texture) in self.textures.values().enumerate() {
gl.ActiveTexture(ffi::TEXTURE0 + i as u32);
gl.BindTexture(ffi::TEXTURE_2D, texture.tex_id());
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_S,
ffi::CLAMP_TO_BORDER as i32,
);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_T,
ffi::CLAMP_TO_BORDER as i32,
);
}
gl.UseProgram(program.program);
for (i, name) in self.textures.keys().enumerate() {
gl.Uniform1i(program.texture_uniforms[name], i as i32);
}
gl.UniformMatrix3fv(
program.uniform_matrix,
1,
ffi::FALSE,
matrix.as_ref().as_ptr(),
);
gl.UniformMatrix3fv(
program.uniform_tex_matrix,
1,
ffi::FALSE,
tex_matrix.as_ref().as_ptr(),
);
gl.Uniform2f(program.uniform_size, dest.size.w as f32, dest.size.h as f32);
gl.Uniform1f(program.uniform_alpha, self.alpha);
for uniform in &self.additional_uniforms {
let desc =
program
.additional_uniforms
.get(&*uniform.name)
.ok_or_else(|| {
GlesError::UnknownUniform(uniform.name.clone().into_owned())
})?;
uniform.value.set(gl, desc)?;
}
gl.EnableVertexAttribArray(program.attrib_vert as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, resources.vbos[0]);
gl.VertexAttribPointer(
program.attrib_vert as u32,
2,
ffi::FLOAT,
ffi::FALSE,
0,
std::ptr::null(),
);
// vert_position
gl.EnableVertexAttribArray(program.attrib_vert_position as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, resources.vbos[1]);
gl.BufferData(
ffi::ARRAY_BUFFER,
(std::mem::size_of::<ffi::types::GLfloat>() * resources.vertices.len())
as isize,
resources.vertices.as_ptr() as *const _,
ffi::STREAM_DRAW,
);
gl.VertexAttribPointer(
program.attrib_vert_position as u32,
4,
ffi::FLOAT,
ffi::FALSE,
0,
std::ptr::null(),
);
let damage_len = damage.len() as i32;
if supports_instancing {
gl.VertexAttribDivisor(program.attrib_vert as u32, 0);
gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1);
gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len);
} else {
// When we have more than 10 rectangles, draw them in batches of 10.
for i in 0..(damage_len - 1) / 10 {
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
// Set damage pointer to the next 10 rectangles.
let offset =
(i + 1) as usize * 6 * 4 * std::mem::size_of::<ffi::types::GLfloat>();
gl.VertexAttribPointer(
program.attrib_vert_position as u32,
4,
ffi::FLOAT,
ffi::FALSE,
0,
offset as *const _,
);
}
// Draw the up to 10 remaining rectangles.
let count = ((damage_len - 1) % 10 + 1) * 6;
gl.DrawArrays(ffi::TRIANGLES, 0, count);
}
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
gl.BindTexture(ffi::TEXTURE_2D, 0);
gl.ActiveTexture(ffi::TEXTURE0);
gl.BindTexture(ffi::TEXTURE_2D, 0);
gl.DisableVertexAttribArray(program.attrib_vert as u32);
gl.DisableVertexAttribArray(program.attrib_vert_position as u32);
}
Ok(())
})??;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>>
for PrimaryGpuPixelShaderWithTexturesRenderElement
{
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
@@ -1,81 +1,12 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ 'static;
}
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
@@ -105,7 +36,7 @@ impl Element for PrimaryGpuTextureRenderElement {
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
@@ -142,16 +73,14 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
}
}
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
for PrimaryGpuTextureRenderElement
{
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
@@ -159,7 +88,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render, 'alloc>,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
+148
View File
@@ -0,0 +1,148 @@
// We need to implement RenderElement manually due to AsGlesFrame requirement.
// This macro does it for us.
#[macro_export]
macro_rules! niri_render_elements {
// The two callable variants: with <R> and without <R>. They include From impls because nested
// repetitions ($type and $variant with + and $R with ?) don't work properly.
($name:ident<R> => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name () ($name<R>) => { $($variant = $type),+ });
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name ($name) () => { $($variant = $type),+ });
$(impl From<$type> for $name {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
// The internal variant that generates most of the code. $name_no_R and $name_R are necessary
// for the impl RenderElement<SomeRenderer> for $name<SomeRenderer>: since $R does not appear
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
#[derive(Debug)]
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
$($variant($type)),+
}
impl$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? smithay::backend::renderer::element::Element for $name$(<$R>)? {
fn id(&self) -> &smithay::backend::renderer::element::Id {
match self {
$($name::$variant(elem) => elem.id()),+
}
}
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
match self {
$($name::$variant(elem) => elem.current_commit()),+
}
}
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> smithay::utils::Rectangle<i32, smithay::utils::Physical> {
match self {
$($name::$variant(elem) => elem.geometry(scale)),+
}
}
fn transform(&self) -> smithay::utils::Transform {
match self {
$($name::$variant(elem) => elem.transform()),+
}
}
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
match self {
$($name::$variant(elem) => elem.src()),+
}
}
fn damage_since(
&self,
scale: smithay::utils::Scale<f64>,
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
) -> smithay::backend::renderer::utils::DamageSet<i32, smithay::utils::Physical> {
match self {
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
}
}
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
match self {
$($name::$variant(elem) => elem.opaque_regions(scale)),+
}
}
fn alpha(&self) -> f32 {
match self {
$($name::$variant(elem) => elem.alpha()),+
}
}
fn kind(&self) -> smithay::backend::renderer::element::Kind {
match self {
$($name::$variant(elem) => elem.kind()),+
}
}
}
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(
&self,
renderer: &mut $crate::backend::tty::TtyRenderer<'render>,
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
};
}
+73
View File
@@ -0,0 +1,73 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use crate::backend::tty::{TtyFrame, TtyRenderer};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ 'static;
}
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
+106
View File
@@ -0,0 +1,106 @@
use std::cell::RefCell;
use std::rc::Rc;
use smithay::backend::renderer::gles::{ffi, Capability, GlesError, GlesFrame, GlesRenderer};
pub struct Resources {
pub vertices: Vec<f32>,
pub vbos: [ffi::types::GLuint; 2],
}
static INSTANCED_VERTS: [ffi::types::GLfloat; 8] = [
1.0, 0.0, // top right
0.0, 0.0, // top left
1.0, 1.0, // bottom right
0.0, 1.0, // bottom left
];
/// Vertices for rendering individual triangles.
const MAX_RECTS_PER_DRAW: usize = 10;
const TRIANGLE_VERTS: [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] = triangle_verts();
const fn triangle_verts() -> [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] {
let mut verts = [0.; 12 * MAX_RECTS_PER_DRAW];
let mut i = 0;
loop {
// Top Left.
verts[i * 12] = 0.0;
verts[i * 12 + 1] = 0.0;
// Bottom left.
verts[i * 12 + 2] = 0.0;
verts[i * 12 + 3] = 1.0;
// Bottom right.
verts[i * 12 + 4] = 1.0;
verts[i * 12 + 5] = 1.0;
// Top left.
verts[i * 12 + 6] = 0.0;
verts[i * 12 + 7] = 0.0;
// Bottom right.
verts[i * 12 + 8] = 1.0;
verts[i * 12 + 9] = 1.0;
// Top right.
verts[i * 12 + 10] = 1.0;
verts[i * 12 + 11] = 0.0;
i += 1;
if i == MAX_RECTS_PER_DRAW {
break;
}
}
verts
}
impl Resources {
fn create(renderer: &mut GlesRenderer) -> Result<Self, GlesError> {
let _span = tracy_client::span!("Resources::init");
let supports_instancing = renderer.capabilities().contains(&Capability::Instancing);
renderer.with_context(|gl| unsafe {
let vertices: &[ffi::types::GLfloat] = if supports_instancing {
&INSTANCED_VERTS
} else {
&TRIANGLE_VERTS
};
let mut vbos = [0; 2];
gl.GenBuffers(vbos.len() as i32, vbos.as_mut_ptr());
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]);
gl.BufferData(
ffi::ARRAY_BUFFER,
std::mem::size_of_val(vertices) as isize,
vertices.as_ptr() as *const _,
ffi::STATIC_DRAW,
);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
Self {
vertices: vec![],
vbos,
}
})
}
pub fn get(frame: &mut GlesFrame) -> Option<Rc<RefCell<Self>>> {
let data = frame.egl_context().user_data();
data.get().cloned()
}
}
pub fn init(renderer: &mut GlesRenderer) {
match Resources::create(renderer) {
Ok(resources) => {
let data = renderer.egl_context().user_data();
if !data.insert_if_missing(|| Rc::new(RefCell::new(resources))) {
error!("resources were already initialized");
}
}
Err(err) => {
warn!("error creating resources for rendering: {err:?}");
}
}
}
+31
View File
@@ -0,0 +1,31 @@
#version 100
precision mediump float;
uniform sampler2D tex_from;
uniform vec2 tex_from_loc;
uniform vec2 tex_from_size;
uniform sampler2D tex_to;
uniform vec2 tex_to_loc;
uniform vec2 tex_to_size;
uniform float alpha;
uniform float amount;
uniform vec2 size;
varying vec2 v_coords;
void main() {
vec2 coords_from = (v_coords - tex_from_loc) / tex_from_size;
vec2 coords_to = (v_coords - tex_to_loc) / tex_to_size;
vec4 color_from = texture2D(tex_from, coords_from);
vec4 color_to = texture2D(tex_to, coords_to);
vec4 color = mix(color_from, color_to, amount);
color = color * alpha;
gl_FragColor = color;
}
@@ -0,0 +1,35 @@
precision mediump float;
uniform float alpha;
#if defined(DEBUG_FLAGS)
uniform float tint;
#endif
uniform vec2 size;
varying vec2 v_coords;
uniform vec4 color_from;
uniform vec4 color_to;
uniform vec2 grad_offset;
uniform float grad_width;
uniform vec2 grad_vec;
void main() {
vec2 coords = v_coords * size + grad_offset;
if ((grad_vec.x < 0.0 && 0.0 <= grad_vec.y) || (0.0 <= grad_vec.x && grad_vec.y < 0.0))
coords.x -= grad_width;
float frac = dot(coords, grad_vec) / dot(grad_vec, grad_vec);
if (grad_vec.y < 0.0)
frac += 1.0;
frac = clamp(frac, 0.0, 1.0);
vec4 out_color = mix(color_from, color_to, frac);
#if defined(DEBUG_FLAGS)
if (tint == 1.0)
out_color = vec4(0.0, 0.3, 0.0, 0.2) + out_color * 0.8;
#endif
gl_FragColor = out_color;
}
+68
View File
@@ -0,0 +1,68 @@
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
use super::primary_gpu_pixel_shader_with_textures::PixelWithTexturesProgram;
use super::renderer::NiriRenderer;
pub struct Shaders {
pub gradient_border: Option<GlesPixelProgram>,
pub crossfade: Option<PixelWithTexturesProgram>,
}
impl Shaders {
fn compile(renderer: &mut GlesRenderer) -> Self {
let _span = tracy_client::span!("Shaders::compile");
let gradient_border = renderer
.compile_custom_pixel_shader(
include_str!("gradient_border.frag"),
&[
UniformName::new("color_from", UniformType::_4f),
UniformName::new("color_to", UniformType::_4f),
UniformName::new("grad_offset", UniformType::_2f),
UniformName::new("grad_width", UniformType::_1f),
UniformName::new("grad_vec", UniformType::_2f),
],
)
.map_err(|err| {
warn!("error compiling gradient border shader: {err:?}");
})
.ok();
let crossfade = PixelWithTexturesProgram::compile(
renderer,
include_str!("crossfade.frag"),
&[
UniformName::new("tex_from_loc", UniformType::_2f),
UniformName::new("tex_from_size", UniformType::_2f),
UniformName::new("tex_to_loc", UniformType::_2f),
UniformName::new("tex_to_size", UniformType::_2f),
UniformName::new("amount", UniformType::_1f),
],
&["tex_from", "tex_to"],
)
.map_err(|err| {
warn!("error compiling crossfade shader: {err:?}");
})
.ok();
Self {
gradient_border,
crossfade,
}
}
pub fn get(renderer: &mut impl NiriRenderer) -> &Self {
let renderer = renderer.as_gles_renderer();
let data = renderer.egl_context().user_data();
data.get()
.expect("shaders::init() must be called when creating the renderer")
}
}
pub fn init(renderer: &mut GlesRenderer) {
let shaders = Shaders::compile(renderer);
let data = renderer.egl_context().user_data();
if !data.insert_if_missing(|| shaders) {
error!("shaders were already compiled");
}
}
+25
View File
@@ -0,0 +1,25 @@
#version 100
uniform mat3 matrix;
uniform mat3 tex_matrix;
attribute vec2 vert;
attribute vec4 vert_position;
varying vec2 v_coords;
mat2 scale(vec2 scale_vec){
return mat2(
scale_vec.x, 0.0,
0.0, scale_vec.y
);
}
void main() {
vec2 vert_transform_translation = vert_position.xy;
vec2 vert_transform_scale = vert_position.zw;
vec3 position = vec3(vert * scale(vert_transform_scale) + vert_transform_translation, 1.0);
v_coords = (tex_matrix * position).xy;
gl_Position = vec4(matrix * position, 1.0);
}
+111
View File
@@ -0,0 +1,111 @@
use std::cell::OnceCell;
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::{render_to_encompassing_texture, RenderTarget, ToRenderElement};
/// Snapshot of a render.
#[derive(Debug)]
pub struct RenderSnapshot<C, B> {
/// Contents for a normal render.
///
/// Relative to the geometry.
pub contents: Vec<C>,
/// Blocked-out contents.
///
/// Relative to the geometry.
pub blocked_out_contents: Vec<B>,
/// Where the contents were blocked out from at the time of the snapshot.
pub block_out_from: Option<BlockOutFrom>,
/// Visual size of the element at the point of the snapshot.
pub size: Size<i32, Logical>,
/// Contents rendered into a texture (lazily).
pub texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
/// Blocked-out contents rendered into a texture (lazily).
pub blocked_out_texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
}
impl<C, B, EC, EB> RenderSnapshot<C, B>
where
C: ToRenderElement<RenderElement = EC>,
B: ToRenderElement<RenderElement = EB>,
EC: RenderElement<GlesRenderer>,
EB: RenderElement<GlesRenderer>,
{
pub fn texture(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
target: RenderTarget,
) -> Option<&(GlesTexture, Rectangle<i32, Physical>)> {
let block_out = match self.block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
};
if block_out {
self.blocked_out_texture.get_or_init(|| {
let _span = tracy_client::span!("RenderSnapshot::Texture");
let elements: Vec<_> = self
.blocked_out_contents
.iter()
.map(|baked| {
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
})
.collect();
match render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
) {
Ok((texture, _sync_point, geo)) => Some((texture, geo)),
Err(err) => {
warn!("error rendering blocked-out contents to texture: {err:?}");
None
}
}
})
} else {
self.texture.get_or_init(|| {
let _span = tracy_client::span!("RenderSnapshot::Texture");
let elements: Vec<_> = self
.contents
.iter()
.map(|baked| {
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
})
.collect();
match render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
) {
Ok((texture, _sync_point, geo)) => Some((texture, geo)),
Err(err) => {
warn!("error rendering contents to texture: {err:?}");
None
}
}
})
}
.as_ref()
}
}
+82
View File
@@ -0,0 +1,82 @@
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::{import_surface, RendererSurfaceStateUserData};
use smithay::backend::renderer::Renderer as _;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point};
use smithay::wayland::compositor::{with_surface_tree_downward, TraversalAction};
use super::BakedBuffer;
/// Renders elements from a surface tree as textures into `storage`.
pub fn render_snapshot_from_surface_tree(
renderer: &mut GlesRenderer,
surface: &WlSurface,
location: Point<i32, Logical>,
storage: &mut Vec<BakedBuffer<TextureBuffer<GlesTexture>>>,
) {
let _span = tracy_client::span!("render_snapshot_from_surface_tree");
with_surface_tree_downward(
surface,
location,
|_, states, location| {
let mut location = *location;
let data = states.data_map.get::<RendererSurfaceStateUserData>();
if let Some(data) = data {
let data = &*data.borrow();
if let Some(view) = data.view() {
location += view.offset;
TraversalAction::DoChildren(location)
} else {
TraversalAction::SkipChildren
}
} else {
TraversalAction::SkipChildren
}
},
|_, states, location| {
let mut location = *location;
let data = states.data_map.get::<RendererSurfaceStateUserData>();
if let Some(data) = data {
if let Some(view) = data.borrow().view() {
location += view.offset;
} else {
return;
}
if let Err(err) = import_surface(renderer, states) {
warn!("failed to import surface: {err:?}");
return;
}
let data = data.borrow();
let view = data.view().unwrap();
let Some(texture) = data.texture::<GlesRenderer>(renderer.id()) else {
return;
};
let buffer = TextureBuffer::from_texture(
renderer,
texture.clone(),
data.buffer_scale(),
data.buffer_transform(),
None,
);
let baked = BakedBuffer {
buffer,
location,
src: Some(view.src),
dst: Some(view.dst),
};
storage.push(baked);
}
},
|_, _, _| true,
);
}
+39
View File
@@ -0,0 +1,39 @@
#[derive(Debug, Clone, Copy)]
pub struct RubberBand {
pub stiffness: f64,
pub limit: f64,
}
impl RubberBand {
pub fn band(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
(1. - (1. / (x * c / d + 1.))) * d
}
pub fn derivative(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
c * d * d / (c * x + d).powi(2)
}
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
let clamped = x.clamp(min, max);
let sign = if x < clamped { -1. } else { 1. };
let diff = (x - clamped).abs();
clamped + sign * self.band(diff)
}
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
if min <= x && x <= max {
return 1.;
}
let clamped = x.clamp(min, max);
let diff = (x - clamped).abs();
self.derivative(diff)
}
}
+40
View File
@@ -0,0 +1,40 @@
pub struct ScrollTracker {
tick: f64,
last: f64,
acc: f64,
}
impl ScrollTracker {
#[allow(clippy::new_without_default)]
pub fn new(tick: i8) -> Self {
Self {
tick: f64::from(tick),
last: 0.,
acc: 0.,
}
}
pub fn accumulate(&mut self, amount: f64) -> i8 {
let changed_direction = (self.last > 0. && amount < 0.) || (self.last < 0. && amount > 0.);
if changed_direction {
self.acc = 0.
}
self.last = amount;
self.acc += amount;
let mut ticks = 0;
if self.acc.abs() >= self.tick {
let clamped = self.acc.clamp(-127. * self.tick, 127. * self.tick);
ticks = (clamped as i16 / self.tick as i16) as i8;
self.acc %= self.tick;
}
ticks
}
pub fn reset(&mut self) {
self.last = 0.;
self.acc = 0.;
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::collections::VecDeque;
use std::time::Duration;
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
const DECELERATION_TOUCHPAD: f64 = 0.997;
#[derive(Debug)]
pub struct SwipeTracker {
history: VecDeque<Event>,
pos: f64,
}
#[derive(Debug, Clone, Copy)]
struct Event {
delta: f64,
timestamp: Duration,
}
impl SwipeTracker {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
history: VecDeque::new(),
pos: 0.,
}
}
/// Pushes a new reading into the tracker.
pub fn push(&mut self, delta: f64, timestamp: Duration) {
// For the events that we care about, timestamps should always increase
// monotonically.
if let Some(last) = self.history.back() {
if timestamp < last.timestamp {
trace!(
"ignoring event with timestamp {timestamp:?} earlier than last {:?}",
last.timestamp
);
return;
}
}
self.history.push_back(Event { delta, timestamp });
self.pos += delta;
self.trim_history();
}
/// Returns the current gesture position.
pub fn pos(&self) -> f64 {
self.pos
}
/// Computes the current gesture velocity.
pub fn velocity(&self) -> f64 {
let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else {
return 0.;
};
let total_time = (last.timestamp - first.timestamp).as_secs_f64();
if total_time == 0. {
return 0.;
}
let total_delta = self.history.iter().map(|event| event.delta).sum::<f64>();
total_delta / total_time
}
/// Computes the gesture end position after decelerating to a halt.
pub fn projected_end_pos(&self) -> f64 {
let vel = self.velocity();
self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln())
}
fn trim_history(&mut self) {
let Some(&Event { timestamp, .. }) = self.history.back() else {
return;
};
while let Some(first) = self.history.front() {
if timestamp <= first.timestamp + HISTORY_LIMIT {
break;
}
let _ = self.history.pop_front();
}
}
}
@@ -1,7 +1,10 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Duration;
use niri_config::Config;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::FontDescription;
use smithay::backend::renderer::element::memory::{
@@ -14,7 +17,7 @@ use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::animation::Animation;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Failed to parse the config file. \
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
@@ -26,6 +29,12 @@ const BORDER: i32 = 4;
pub struct ConfigErrorNotification {
state: State,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
// notification.
created_path: Option<PathBuf>,
config: Rc<RefCell<Config>>,
}
enum State {
@@ -39,16 +48,37 @@ pub type ConfigErrorNotificationRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ConfigErrorNotification {
pub fn new() -> Self {
pub fn new(config: Rc<RefCell<Config>>) -> Self {
Self {
state: State::Hidden,
buffers: RefCell::new(HashMap::new()),
created_path: None,
config,
}
}
fn animation(&self, from: f64, to: f64) -> Animation {
let c = self.config.borrow();
Animation::new(from, to, 0., c.animations.config_notification_open_close.0)
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
}
self.state = State::Showing(self.animation(0., 1.));
}
pub fn show(&mut self) {
if self.created_path.is_some() {
self.created_path = None;
self.buffers.borrow_mut().clear();
}
// Show from scratch even if already showing to bring attention.
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
self.state = State::Showing(self.animation(0., 1.));
}
pub fn hide(&mut self) {
@@ -56,7 +86,7 @@ impl ConfigErrorNotification {
return;
}
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
self.state = State::Hiding(self.animation(1., 0.));
}
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
@@ -65,7 +95,15 @@ impl ConfigErrorNotification {
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
let duration = if self.created_path.is_some() {
// Make this quite a bit longer because it comes with a monitor modeset
// (can take a while) and an important hotkeys popup diverting the
// attention.
Duration::from_secs(8)
} else {
Duration::from_secs(4)
};
self.state = State::Shown(target_presentation_time + duration);
}
}
State::Shown(deadline) => {
@@ -75,7 +113,7 @@ impl ConfigErrorNotification {
}
State::Hiding(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
if anim.is_clamped_done() {
self.state = State::Hidden;
}
}
@@ -96,11 +134,12 @@ impl ConfigErrorNotification {
}
let scale = output.current_scale().integer_scale();
let path = self.created_path.as_deref();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale).ok());
.or_insert_with_key(move |&scale| render(scale, path).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
@@ -138,19 +177,30 @@ impl ConfigErrorNotification {
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut text = String::from(TEXT);
let mut border_color = (1., 0.3, 0.3);
if let Some(path) = created_path {
text = format!(
"Created a default config file at \
<span face='monospace' bgcolor='#000000'>{:?}</span>",
path
);
border_color = (0.5, 1., 0.5);
};
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
@@ -166,25 +216,25 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(1., 0.3, 0.3);
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -12,7 +12,7 @@ use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
@@ -111,7 +111,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
@@ -130,13 +130,13 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::rc::Rc;
use niri_config::{Action, Config, Key, Modifiers};
use niri_config::{Action, Config, Key, Modifiers, Trigger};
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
use smithay::backend::renderer::element::memory::{
@@ -18,7 +18,7 @@ use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::{Physical, Size, Transform};
use crate::input::CompositorMod;
use crate::render_helpers::NiriRenderer;
use crate::render_helpers::renderer::NiriRenderer;
const PADDING: i32 = 8;
const MARGIN: i32 = PADDING * 2;
@@ -155,13 +155,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
let binds = &config.binds.0;
// Collect actions that we want to show.
let mut actions = vec![
&Action::ShowHotkeyOverlay,
&Action::Quit,
&Action::CloseWindow,
];
let mut actions = vec![&Action::ShowHotkeyOverlay];
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
// Quit(false).
if binds.iter().any(|bind| bind.action == Action::Quit(false)) {
actions.push(&Action::Quit(false));
} else if binds.iter().any(|bind| bind.action == Action::Quit(true)) {
actions.push(&Action::Quit(true));
} else {
actions.push(&Action::Quit(false));
}
actions.extend(&[
&Action::CloseWindow,
&Action::FocusColumnLeft,
&Action::FocusColumnRight,
&Action::MoveColumnLeft,
@@ -173,12 +180,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown))
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
{
actions.push(&Action::MoveColumnToWorkspaceDown);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
{
actions.push(&Action::MoveWindowToWorkspaceDown);
} else {
@@ -188,12 +195,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
// Same for -up.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
{
actions.push(&Action::MoveColumnToWorkspaceUp);
} else if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
{
actions.push(&Action::MoveWindowToWorkspaceUp);
} else {
@@ -208,20 +215,28 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
]);
// Screenshot is not as important, can omit if not bound.
if binds
.iter()
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
{
if binds.iter().any(|bind| bind.action == Action::Screenshot) {
actions.push(&Action::Screenshot);
}
// Add the spawn actions.
for bind in binds
.iter()
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
{
actions.push(bind.actions.first().unwrap());
let mut spawn_actions = Vec::new();
for bind in binds.iter().filter(|bind| {
matches!(bind.action, Action::Spawn(_))
// Only show binds with Mod or Super to filter out stuff like volume up/down.
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|| bind.key.modifiers.contains(Modifiers::SUPER))
// Also filter out wheel and touchpad scroll binds.
&& matches!(bind.key.trigger, Trigger::Keysym(_))
}) {
let action = &bind.action;
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
if !spawn_actions.contains(&action) {
spawn_actions.push(action);
}
}
actions.extend(spawn_actions);
let strings = actions
.into_iter()
@@ -230,7 +245,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
.binds
.0
.iter()
.find(|bind| bind.actions.first() == Some(action))
.find(|bind| bind.action == *action)
.map(|bind| key_name(comp_mod, &bind.key))
.unwrap_or_else(|| String::from("(not bound)"));
@@ -243,7 +258,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
let bold = AttrList::new();
@@ -298,7 +313,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
cr.set_source_rgb(1., 1., 1.);
@@ -306,20 +321,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
layout.set_attributes(Some(&attrs));
layout.set_text(key);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to((key_width + padding).into(), 0.);
layout.set_attributes(None);
layout.set_markup(action);
pangocairo::show_layout(&cr, &layout);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to(
(-(key_width + padding)).into(),
@@ -338,7 +353,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
@@ -356,7 +371,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
fn action_name(action: &Action) -> String {
match action {
Action::Quit => String::from("Exit niri"),
Action::Quit(_) => String::from("Exit niri"),
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
Action::CloseWindow => String::from("Close Focused Window"),
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
@@ -401,7 +416,19 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
if key.modifiers.contains(Modifiers::CTRL) {
name.push_str("Ctrl + ");
}
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
let pretty = match key.trigger {
Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)),
Trigger::WheelScrollDown => String::from("Wheel Scroll Down"),
Trigger::WheelScrollUp => String::from("Wheel Scroll Up"),
Trigger::WheelScrollLeft => String::from("Wheel Scroll Left"),
Trigger::WheelScrollRight => String::from("Wheel Scroll Right"),
Trigger::TouchpadScrollDown => String::from("Touchpad Scroll Down"),
Trigger::TouchpadScrollUp => String::from("Touchpad Scroll Up"),
Trigger::TouchpadScrollLeft => String::from("Touchpad Scroll Left"),
Trigger::TouchpadScrollRight => String::from("Touchpad Scroll Right"),
};
name.push_str(&pretty);
name
}
+4
View File
@@ -0,0 +1,4 @@
pub mod config_error_notification;
pub mod exit_confirm_dialog;
pub mod hotkey_overlay;
pub mod screenshot_ui;
+40 -164
View File
@@ -10,16 +10,16 @@ use smithay::backend::allocator::Fourcc;
use smithay::backend::input::{ButtonState, MouseButton};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::ExportMem;
use smithay::input::keyboard::{Keysym, ModifiersState};
use smithay::output::{Output, WeakOutput};
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::PrimaryGpuTextureRenderElement;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::RenderTarget;
const BORDER: i32 = 2;
@@ -42,16 +42,19 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
transform: Transform,
// Output, screencast, screen capture.
texture: [GlesTexture; 3],
texture_buffer: [TextureBuffer<GlesTexture>; 3],
buffers: [SolidColorBuffer; 8],
locations: [Point<i32, Physical>; 8],
}
#[derive(Debug)]
pub enum ScreenshotUiRenderElement {
Screenshot(PrimaryGpuTextureRenderElement),
SolidColor(SolidColorRenderElement),
niri_render_elements! {
ScreenshotUiRenderElement => {
Screenshot = PrimaryGpuTextureRenderElement,
SolidColor = SolidColorRenderElement,
}
}
impl ScreenshotUi {
@@ -64,7 +67,8 @@ impl ScreenshotUi {
pub fn open(
&mut self,
renderer: &GlesRenderer,
screenshots: HashMap<Output, GlesTexture>,
// Output, screencast, screen capture.
screenshots: HashMap<Output, [GlesTexture; 3]>,
default_output: Output,
) -> bool {
if screenshots.is_empty() {
@@ -94,6 +98,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -104,17 +109,13 @@ 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(),
scale,
Transform::Normal,
None,
);
let texture_buffer = texture.clone().map(|texture| {
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None)
});
let buffers = [
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
@@ -129,6 +130,7 @@ impl ScreenshotUi {
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -240,7 +242,11 @@ impl ScreenshotUi {
}
}
pub fn render_output(&self, output: &Output) -> ArrayVec<ScreenshotUiRenderElement, 9> {
pub fn render_output(
&self,
output: &Output,
target: RenderTarget,
) -> ArrayVec<ScreenshotUiRenderElement, 9> {
let _span = tracy_client::span!("ScreenshotUi::render_output");
let Self::Open { output_data, .. } = self else {
@@ -266,10 +272,15 @@ impl ScreenshotUi {
}));
// The screenshot itself goes last.
let index = match target {
RenderTarget::Output => 0,
RenderTarget::Screencast => 1,
RenderTarget::ScreenCapture => 2,
};
elements.push(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
(0., 0.),
&output_data.texture_buffer,
&output_data.texture_buffer[index],
None,
None,
None,
@@ -304,7 +315,7 @@ impl ScreenshotUi {
.to_buffer(1, Transform::Normal, &data.size.to_logical(1));
let mapping = renderer
.copy_texture(&data.texture, buf_rect, Fourcc::Abgr8888)
.copy_texture(&data.texture[0], buf_rect, Fourcc::Abgr8888)
.context("error copying texture")?;
let copy = renderer
.map_texture(&mapping)
@@ -313,12 +324,12 @@ impl ScreenshotUi {
Ok((rect.size, copy.to_vec()))
}
pub fn action(&self, raw: Option<Keysym>, mods: ModifiersState) -> Option<Action> {
pub fn action(&self, raw: Keysym, mods: ModifiersState) -> Option<Action> {
if !matches!(self, Self::Open { .. }) {
return None;
}
action(raw?, mods)
action(raw, mods)
}
pub fn selection_output(&self) -> Option<&Output> {
@@ -333,10 +344,10 @@ impl ScreenshotUi {
}
}
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
if let Self::Open { output_data, .. } = self {
let data = output_data.get(output)?;
Some((data.size, data.scale))
Some((data.size, data.scale, data.transform))
} else {
None
}
@@ -448,138 +459,3 @@ pub fn rect_from_corner_points(
let y2 = max(a.y, b.y);
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
}
// Manual RenderElement implementation due to AsGlesFrame requirement.
impl Element for ScreenshotUiRenderElement {
fn id(&self) -> &Id {
match self {
Self::Screenshot(elem) => elem.id(),
Self::SolidColor(elem) => elem.id(),
}
}
fn current_commit(&self) -> CommitCounter {
match self {
Self::Screenshot(elem) => elem.current_commit(),
Self::SolidColor(elem) => elem.current_commit(),
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
match self {
Self::Screenshot(elem) => elem.geometry(scale),
Self::SolidColor(elem) => elem.geometry(scale),
}
}
fn transform(&self) -> Transform {
match self {
Self::Screenshot(elem) => elem.transform(),
Self::SolidColor(elem) => elem.transform(),
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
match self {
Self::Screenshot(elem) => elem.src(),
Self::SolidColor(elem) => elem.src(),
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
match self {
Self::Screenshot(elem) => elem.damage_since(scale, commit),
Self::SolidColor(elem) => elem.damage_since(scale, commit),
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
match self {
Self::Screenshot(elem) => elem.opaque_regions(scale),
Self::SolidColor(elem) => elem.opaque_regions(scale),
}
}
fn alpha(&self) -> f32 {
match self {
Self::Screenshot(elem) => elem.alpha(),
Self::SolidColor(elem) => elem.alpha(),
}
}
fn kind(&self) -> Kind {
match self {
Self::Screenshot(elem) => elem.kind(),
Self::SolidColor(elem) => elem.kind(),
}
}
}
impl RenderElement<GlesRenderer> for ScreenshotUiRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
match self {
Self::Screenshot(elem) => {
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
}
Self::SolidColor(elem) => {
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
}
}
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for ScreenshotUiRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'render, 'alloc, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
match self {
Self::Screenshot(elem) => {
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
}
Self::SolidColor(elem) => {
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
}
}
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render, 'alloc>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl From<SolidColorRenderElement> for ScreenshotUiRenderElement {
fn from(x: SolidColorRenderElement) -> Self {
Self::SolidColor(x)
}
}
impl From<PrimaryGpuTextureRenderElement> for ScreenshotUiRenderElement {
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
Self::Screenshot(x)
}
}
+27
View File
@@ -0,0 +1,27 @@
use std::sync::atomic::{AtomicU32, Ordering};
/// Counter that returns unique IDs.
///
/// Under the hood it uses a `u32` that will eventually wrap around. When incrementing it once a
/// second, it will wrap around after about 136 years.
pub struct IdCounter {
value: AtomicU32,
}
impl IdCounter {
pub const fn new() -> Self {
Self {
value: AtomicU32::new(0),
}
}
pub fn next(&self) -> u32 {
self.value.fetch_add(1, Ordering::SeqCst)
}
}
impl Default for IdCounter {
fn default() -> Self {
Self::new()
}
}
+68 -89
View File
@@ -1,23 +1,31 @@
use std::ffi::{CString, OsStr};
use std::io::{self, Write};
use std::io::Write;
use std::os::unix::prelude::OsStrExt;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::path::{Path, PathBuf};
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{ensure, Context};
use directories::UserDirs;
use git_version::git_version;
use niri_config::Config;
use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
pub mod id;
pub mod spawning;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
pub fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
pub fn get_monotonic_time() -> Duration {
@@ -29,6 +37,10 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
rect.loc + rect.size.downscale(2).to_point()
}
pub fn center_f64(rect: Rectangle<f64, Logical>) -> Point<f64, Logical> {
rect.loc + rect.size.downscale(2.0).to_point()
}
pub fn output_size(output: &Output) -> Size<i32, Logical> {
let output_scale = output.current_scale().integer_scale();
let output_transform = output.current_transform();
@@ -39,6 +51,51 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
.to_logical(output_scale)
}
pub fn logical_output(output: &Output) -> niri_ipc::LogicalOutput {
let loc = output.current_location();
let size = output_size(output);
let transform = match output.current_transform() {
Transform::Normal => niri_ipc::Transform::Normal,
Transform::_90 => niri_ipc::Transform::_90,
Transform::_180 => niri_ipc::Transform::_180,
Transform::_270 => niri_ipc::Transform::_270,
Transform::Flipped => niri_ipc::Transform::Flipped,
Transform::Flipped90 => niri_ipc::Transform::Flipped90,
Transform::Flipped180 => niri_ipc::Transform::Flipped180,
Transform::Flipped270 => niri_ipc::Transform::Flipped270,
};
niri_ipc::LogicalOutput {
x: loc.x,
y: loc.y,
width: size.w as u32,
height: size.h as u32,
scale: output.current_scale().fractional_scale(),
transform,
}
}
pub fn ipc_transform_to_smithay(transform: niri_ipc::Transform) -> Transform {
match transform {
niri_ipc::Transform::Normal => Transform::Normal,
niri_ipc::Transform::_90 => Transform::_90,
niri_ipc::Transform::_180 => Transform::_180,
niri_ipc::Transform::_270 => Transform::_270,
niri_ipc::Transform::Flipped => Transform::Flipped,
niri_ipc::Transform::Flipped90 => Transform::Flipped90,
niri_ipc::Transform::Flipped180 => Transform::Flipped180,
niri_ipc::Transform::Flipped270 => Transform::Flipped270,
}
}
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
Ok(Some([dirs.home_dir(), rest].iter().collect()))
} else {
Ok(None)
}
}
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
let Some(path) = &config.screenshot_path else {
return Ok(None);
@@ -61,91 +118,13 @@ pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>>
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
}
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
path = [dirs.home_dir(), rest].iter().collect();
if let Some(expanded) = expand_home(&path).context("error expanding ~")? {
path = expanded;
}
Ok(Some(path))
}
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let command = command.as_ref();
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Double-fork to avoid having to waitpid the child.
unsafe {
process.pre_exec(|| {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
Ok(())
});
}
let mut child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return;
}
};
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
pub fn write_png_rgba8(
w: impl Write,
width: u32,
+378
View File
@@ -0,0 +1,378 @@
use std::ffi::OsStr;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::{io, thread};
use niri_config::Environment;
use crate::utils::expand_home;
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let mut command = command.as_ref();
// Expand `~` at the start.
let expanded = expand_home(Path::new(command));
match &expanded {
Ok(Some(expanded)) => command = expanded.as_ref(),
Ok(None) => (),
Err(err) => {
warn!("error expanding ~: {err:?}");
}
}
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Set configured environment.
let env = CHILD_ENV.read().unwrap();
for var in &env.0 {
if let Some(value) = &var.value {
process.env(&var.name, value);
} else {
process.env_remove(&var.name);
}
}
drop(env);
let Some(mut child) = do_spawn(command, process) else {
return;
};
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
#[cfg(not(feature = "systemd"))]
fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
unsafe {
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
Ok(())
});
}
let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return None;
}
};
Some(child)
}
#[cfg(feature = "systemd")]
use systemd::do_spawn;
#[cfg(feature = "systemd")]
mod systemd {
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use smithay::reexports::rustix;
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
use super::*;
pub fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
use libc::close_range;
// When running as a systemd session, we want to put children into their own transient
// scopes in order to separate them from the niri process. This is helpful for
// example to prevent the OOM killer from taking down niri together with a
// misbehaving client.
//
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits
// at just the right time, the transient unit will be created but empty, so it will
// linger around forever.
//
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
// intermediate child we will send back the grandchild PID, and in niri we will create a
// transient scope with both our intermediate child and the grandchild PIDs set. Only then
// we will signal our intermediate child to exit. This way, even if the grandchild
// exits quickly, a non-empty scope will be created (with just our intermediate
// child), then cleaned up when our intermediate child exits.
// Make a pipe to receive the grandchild PID.
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe to transfer child PID: {err:?}");
})
.ok()
.unzip();
// Make a pipe to wait in the intermediate child.
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe for child to wait on: {err:?}");
})
.ok()
.unzip();
unsafe {
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
// the raw fd inside so that it's not closed any extra times.
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
// Close FDs that we don't need. Especially important for the write ones to unblock
// the readers.
if let Some(fd) = pipe_pid_read_fd.take() {
close(fd);
}
if let Some(fd) = pipe_wait_write_fd.take() {
close(fd);
}
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
grandchild_pid => {
// Send back the PID.
if let Some(pipe) = pipe_pid_write {
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
}
// Wait until the parent signals us to exit.
if let Some(pipe) = pipe_wait_read {
// We're going to exit afterwards. Close all other FDs to allow
// Command::spawn() to return in the parent process.
let raw = pipe.as_raw_fd() as u32;
let _ = close_range(0, raw - 1, 0);
let _ = close_range(raw + 1, !0, 0);
let _ = read_all(pipe, &mut [0]);
}
libc::_exit(0)
}
}
Ok(())
});
}
let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return None;
}
};
drop(pipe_pid_write);
drop(pipe_wait_read);
// Wait for the grandchild PID.
if let Some(pipe) = pipe_pid_read {
let mut buf = [0; 4];
match read_all(pipe, &mut buf) {
Ok(()) => {
let pid = i32::from_ne_bytes(buf);
trace!("spawned PID: {pid}");
// Start a systemd scope for the grandchild.
#[cfg(feature = "systemd")]
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
trace!("error starting systemd scope for spawned command: {err:?}");
}
}
Err(err) => {
warn!("error reading child PID: {err:?}");
}
}
}
// Signal the intermediate child to exit now that we're done trying to creating a systemd
// scope.
trace!("signaling child to exit");
drop(pipe_wait_write);
Some(child)
}
#[cfg(feature = "systemd")]
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
let mut written = 0;
loop {
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
written += n;
if written == buf.len() {
return Ok(());
}
}
}
#[cfg(feature = "systemd")]
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
let mut start = 0;
loop {
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
start += n;
if start == buf.len() {
return Ok(());
}
}
}
/// Puts a (newly spawned) pid into a transient systemd scope.
///
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
/// from bringing down the compositor together with a misbehaving client.
#[cfg(feature = "systemd")]
fn start_systemd_scope(
name: &OsStr,
intermediate_pid: u32,
child_pid: u32,
) -> anyhow::Result<()> {
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
use anyhow::Context;
use zbus::zvariant::{OwnedObjectPath, Value};
use crate::utils::IS_SYSTEMD_SERVICE;
// We only start transient scopes if we're a systemd service ourselves.
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
return Ok(());
}
let _span = tracy_client::span!();
// Extract the basename.
let name = Path::new(name).file_name().unwrap_or(name);
let mut scope_name = String::from("app-niri-");
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
// systemd source.
for &c in name.as_bytes() {
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
scope_name.push(char::from(c));
} else {
let _ = write!(scope_name, "\\x{c:02x}");
}
}
let _ = write!(scope_name, "-{child_pid}.scope");
// Ask systemd to start a transient scope.
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
let conn = CONNECTION
.get_or_init(zbus::blocking::Connection::session)
.clone()
.context("error connecting to session bus")?;
let proxy = zbus::blocking::Proxy::new(
&conn,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
)
.context("error creating a Proxy")?;
let signals = proxy
.receive_signal("JobRemoved")
.context("error creating a signal iterator")?;
let pids: &[_] = &[intermediate_pid, child_pid];
let properties: &[_] = &[
("PIDs", Value::new(pids)),
("CollectMode", Value::new("inactive-or-failed")),
];
let aux: &[(&str, &[(&str, Value)])] = &[];
let job: OwnedObjectPath = proxy
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
.context("error calling StartTransientUnit")?;
trace!("waiting for JobRemoved");
for message in signals {
let body: (u32, OwnedObjectPath, &str, &str) =
message.body().context("error parsing signal")?;
if body.1 == job {
// Our transient unit had started, we're good to exit the intermediate child.
break;
}
}
Ok(())
}
}
+348
View File
@@ -0,0 +1,348 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
Self::with_start_notification(path, changed, None)
}
pub fn with_start_notification(
path: PathBuf,
changed: SyncSender<()>,
started: Option<mpsc::SyncSender<()>>,
) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
// this "should" be as simple as mtime, but it does not quite work in practice;
// it doesn't work if the config is a symlink, and its target changes but the
// new target and old target have identical mtimes.
//
// in practice, this does not occur on any systems other than nix.
// because, on nix practically everything is a symlink to /nix/store
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
let mut last_props = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
.ok();
if let Some(started) = started {
let _ = started.send(());
}
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(new_props) = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
{
if last_props.as_ref() != Some(&new_props) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_props = Some(new_props);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::AtomicU8;
use calloop::channel::sync_channel;
use calloop::EventLoop;
use smithay::reexports::rustix::fs::{futimens, Timestamps};
use smithay::reexports::rustix::time::Timespec;
use xshell::{cmd, Shell};
use super::*;
fn check(
setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
) {
let sh = Shell::new().unwrap();
let temp_dir = sh.create_temp_dir().unwrap();
sh.change_dir(temp_dir.path());
// let dir = sh.create_dir("xshell").unwrap();
// sh.change_dir(dir);
let mut config_path = sh.current_dir();
config_path.push("niri");
config_path.push("config.kdl");
setup(&sh).unwrap();
let changed = AtomicU8::new(0);
let mut event_loop = EventLoop::try_new().unwrap();
let loop_handle = event_loop.handle();
let (tx, rx) = sync_channel(1);
let (started_tx, started_rx) = mpsc::sync_channel(1);
let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
loop_handle
.insert_source(rx, |_, _, _| {
changed.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
started_rx.recv().unwrap();
// HACK: if we don't sleep, files might have the same mtime.
thread::sleep(Duration::from_millis(100));
change(&sh).unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 1);
// Verify that the watcher didn't break.
sh.write_file(&config_path, "c").unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 2);
}
#[test]
fn change_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn create_file() {
check(
|sh| {
sh.create_dir("niri")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn create_dir_and_file() {
check(
|_sh| Ok(()),
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn change_linked_file() {
check(
|sh| {
sh.write_file("niri/config2.kdl", "a")?;
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
sh.write_file("niri/config2.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn change_file_in_linked_dir() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri/config.kdl")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn swap_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
sh.remove_path("niri")?;
cmd!(sh, "mv niri2 niri").run()?;
Ok(())
},
);
}
#[test]
fn swap_just_link() {
// NixOS setup: link path changes, mtime stays constant.
check(
|sh| {
let mut dir = sh.current_dir();
dir.push("niri");
sh.create_dir(&dir)?;
let mut d2 = dir.clone();
d2.push("config2.kdl");
let mut c2 = File::create(d2).unwrap();
write!(c2, "a")?;
c2.flush()?;
futimens(
&c2,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c2.sync_all()?;
drop(c2);
let mut d3 = dir.clone();
d3.push("config3.kdl");
let mut c3 = File::create(d3).unwrap();
write!(c3, "b")?;
c3.flush()?;
futimens(
&c3,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c3.sync_all()?;
drop(c3);
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
cmd!(sh, "unlink niri/config.kdl").run()?;
cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
Ok(())
},
);
}
#[test]
fn swap_dir_link() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri3/config.kdl", "b")?;
cmd!(sh, "unlink niri").run()?;
cmd!(sh, "ln -s niri3 niri").run()?;
Ok(())
},
);
}
}
-60
View File
@@ -1,60 +0,0 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_mtime = Some(mtime);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
+394
View File
@@ -0,0 +1,394 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use niri_config::{BlockOutFrom, WindowRule};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{AsRenderElements, Id, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::space::SpaceElement as _;
use smithay::desktop::{PopupManager, Window};
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_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use smithay::wayland::compositor::{
remove_pre_commit_hook, send_surface_state, with_states, HookId,
};
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use super::{ResolvedWindowRules, WindowRef};
use crate::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
use crate::niri::WindowOffscreenId;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::surface::render_snapshot_from_surface_tree;
use crate::render_helpers::{BakedBuffer, RenderTarget};
#[derive(Debug)]
pub struct Mapped {
pub window: Window,
/// Pre-commit hook that we have on all mapped toplevel surfaces.
pre_commit_hook: HookId,
/// Up-to-date rules.
rules: ResolvedWindowRules,
/// Whether the window rules need to be recomputed.
///
/// This is not used in all cases; for example, app ID and title changes recompute the rules
/// immediately, rather than setting this flag.
need_to_recompute_rules: bool,
/// Whether this window has the keyboard focus.
is_focused: bool,
/// Buffer to draw instead of the window when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
/// Snapshot of the last render for use in the close animation.
unmap_snapshot: RefCell<Option<LayoutElementRenderSnapshot>>,
/// Whether the next configure should be animated, if the configured state changed.
animate_next_configure: bool,
/// Serials of commits that should be animated.
animate_serials: Vec<Serial>,
/// Snapshot right before an animated commit.
animation_snapshot: Option<LayoutElementRenderSnapshot>,
}
impl Mapped {
pub fn new(window: Window, rules: ResolvedWindowRules, hook: HookId) -> Self {
Self {
window,
pre_commit_hook: hook,
rules,
need_to_recompute_rules: false,
is_focused: false,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])),
unmap_snapshot: RefCell::new(None),
animate_next_configure: false,
animate_serials: Vec::new(),
animation_snapshot: None,
}
}
pub fn toplevel(&self) -> &ToplevelSurface {
self.window.toplevel().expect("no X11 support")
}
/// Recomputes the resolved window rules and returns whether they changed.
pub fn recompute_window_rules(&mut self, rules: &[WindowRule]) -> bool {
self.need_to_recompute_rules = false;
let new_rules = ResolvedWindowRules::compute(rules, WindowRef::Mapped(self));
if new_rules == self.rules {
return false;
}
self.rules = new_rules;
true
}
pub fn recompute_window_rules_if_needed(&mut self, rules: &[WindowRule]) -> bool {
if !self.need_to_recompute_rules {
return false;
}
self.recompute_window_rules(rules)
}
pub fn is_focused(&self) -> bool {
self.is_focused
}
pub fn set_is_focused(&mut self, is_focused: bool) {
if self.is_focused == is_focused {
return;
}
self.is_focused = is_focused;
self.need_to_recompute_rules = true;
}
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> LayoutElementRenderSnapshot {
let _span = tracy_client::span!("Mapped::render_snapshot");
let size = self.size();
let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(size);
let blocked_out_contents = vec![BakedBuffer {
buffer: buffer.clone(),
location: Point::from((0, 0)),
src: None,
dst: None,
}];
let buf_pos = self.window.geometry().loc.upscale(-1);
let mut contents = vec![];
let surface = self.toplevel().wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
let offset = self.window.geometry().loc + popup_offset - popup.geometry().loc;
render_snapshot_from_surface_tree(
renderer,
popup.wl_surface(),
buf_pos + offset,
&mut contents,
);
}
render_snapshot_from_surface_tree(renderer, surface, buf_pos, &mut contents);
RenderSnapshot {
contents,
blocked_out_contents,
block_out_from: self.rules().block_out_from,
size,
texture: Default::default(),
blocked_out_texture: Default::default(),
}
}
pub fn store_unmap_snapshot_if_empty(&self, renderer: &mut GlesRenderer) {
let mut snapshot = self.unmap_snapshot.borrow_mut();
if snapshot.is_some() {
return;
}
*snapshot = Some(self.render_snapshot(renderer));
}
pub fn should_animate_commit(&mut self, commit_serial: Serial) -> bool {
let mut should_animate = false;
self.animate_serials.retain_mut(|serial| {
if commit_serial.is_no_older_than(serial) {
should_animate = true;
false
} else {
true
}
});
should_animate
}
pub fn store_animation_snapshot(&mut self, renderer: &mut GlesRenderer) {
self.animation_snapshot = Some(self.render_snapshot(renderer));
}
}
impl Drop for Mapped {
fn drop(&mut self) {
remove_pre_commit_hook(self.toplevel().wl_surface(), self.pre_commit_hook);
}
}
impl LayoutElement for Mapped {
type Id = Window;
fn id(&self) -> &Self::Id {
&self.window
}
fn size(&self) -> Size<i32, Logical> {
self.window.geometry().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
Point::from((0, 0)) - self.window.geometry().loc
}
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
let surface_local = point + self.window.geometry().loc.to_f64();
self.window.is_in_input_region(&surface_local)
}
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
let block_out = match self.rules.block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
};
if block_out {
let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(self.window.geometry().size);
let elem = SolidColorRenderElement::from_buffer(
&buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
vec![elem.into()]
} else {
let buf_pos = location - self.window.geometry().loc;
let buf_pos = buf_pos.to_physical_precise_round(scale);
self.window.render_elements(renderer, buf_pos, scale, alpha)
}
}
fn request_size(&mut self, size: Size<i32, Logical>, animate: bool) {
let changed = self.toplevel().with_pending_state(|state| {
let changed = state.size != Some(size);
state.size = Some(size);
state.states.unset(xdg_toplevel::State::Fullscreen);
changed
});
if changed && animate {
self.animate_next_configure = true;
}
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.size = Some(size);
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
fn min_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
});
if let Some(x) = self.rules.min_width {
size.w = max(size.w, i32::from(x));
}
if let Some(x) = self.rules.min_height {
size.h = max(size.h, i32::from(x));
}
size
}
fn max_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
});
if let Some(x) = self.rules.max_width {
if size.w == 0 {
size.w = i32::from(x);
} else if x > 0 {
size.w = min(size.w, i32::from(x));
}
}
if let Some(x) = self.rules.max_height {
if size.h == 0 {
size.h = i32::from(x);
} else if x > 0 {
size.h = min(size.h, i32::from(x));
}
}
size
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
self.toplevel().wl_surface() == wl_surface
}
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
self.window.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
fn has_ssd(&self) -> bool {
self.toplevel().current_state().decoration_mode
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
}
fn output_enter(&self, output: &Output) {
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
self.window.output_enter(output, overlap)
}
fn output_leave(&self, output: &Output) {
self.window.output_leave(output)
}
fn set_offscreen_element_id(&self, id: Option<Id>) {
let data = self
.window
.user_data()
.get_or_insert(WindowOffscreenId::default);
data.0.replace(id);
}
fn set_activated(&mut self, active: bool) {
let changed = self.toplevel().with_pending_state(|state| {
if active {
state.states.set(xdg_toplevel::State::Activated)
} else {
state.states.unset(xdg_toplevel::State::Activated)
}
});
self.need_to_recompute_rules |= changed;
}
fn set_bounds(&self, bounds: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.bounds = Some(bounds);
});
}
fn send_pending_configure(&mut self) {
if let Some(serial) = self.toplevel().send_pending_configure() {
if self.animate_next_configure {
self.animate_serials.push(serial);
}
}
self.animate_next_configure = false;
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.current_state()
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
fn refresh(&self) {
self.window.refresh();
}
fn rules(&self) -> &ResolvedWindowRules {
&self.rules
}
fn take_unmap_snapshot(&self) -> Option<LayoutElementRenderSnapshot> {
self.unmap_snapshot.take()
}
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
self.animation_snapshot.as_ref()
}
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
self.animation_snapshot.take()
}
}
+218
View File
@@ -0,0 +1,218 @@
use niri_config::{BlockOutFrom, Match, WindowRule};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use crate::layout::workspace::ColumnWidth;
pub mod mapped;
pub use mapped::Mapped;
pub mod unmapped;
pub use unmapped::{InitialConfigureState, Unmapped};
/// Reference to a mapped or unmapped window.
#[derive(Debug, Clone, Copy)]
pub enum WindowRef<'a> {
Unmapped(&'a Unmapped),
Mapped(&'a Mapped),
}
/// Rules fully resolved for a window.
#[derive(Debug, PartialEq)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
/// Extra bound on the minimum window width.
pub min_width: Option<u16>,
/// Extra bound on the minimum window height.
pub min_height: Option<u16>,
/// Extra bound on the maximum window width.
pub max_width: Option<u16>,
/// Extra bound on the maximum window height.
pub max_height: Option<u16>,
/// Whether or not to draw the border with a solid background.
///
/// `None` means using the SSD heuristic.
pub draw_border_with_background: Option<bool>,
/// Extra opacity to draw this window with.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
}
impl<'a> WindowRef<'a> {
pub fn toplevel(self) -> &'a ToplevelSurface {
match self {
WindowRef::Unmapped(unmapped) => unmapped.toplevel(),
WindowRef::Mapped(mapped) => mapped.toplevel(),
}
}
pub fn is_focused(self) -> bool {
match self {
WindowRef::Unmapped(_) => false,
WindowRef::Mapped(mapped) => mapped.is_focused(),
}
}
}
impl ResolvedWindowRules {
pub const fn empty() -> Self {
Self {
default_width: None,
open_on_output: None,
open_maximized: None,
open_fullscreen: None,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
draw_border_with_background: None,
opacity: None,
block_out_from: None,
}
}
pub fn compute(rules: &[WindowRule], window: WindowRef) -> Self {
let _span = tracy_client::span!("ResolvedWindowRules::compute");
let mut resolved = ResolvedWindowRules::empty();
let toplevel = window.toplevel();
with_states(toplevel.wl_surface(), |states| {
let mut role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
// Ensure server_pending like in Smithay's with_pending_state().
if role.server_pending.is_none() {
role.server_pending = Some(role.current_server_state().clone());
}
let mut open_on_output = None;
for rule in rules {
let matches = |m| window_matches(window, &role, m);
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}
if rule.excludes.iter().any(matches) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
if let Some(x) = rule.min_width {
resolved.min_width = Some(x);
}
if let Some(x) = rule.min_height {
resolved.min_height = Some(x);
}
if let Some(x) = rule.max_width {
resolved.max_width = Some(x);
}
if let Some(x) = rule.max_height {
resolved.max_height = Some(x);
}
if let Some(x) = rule.draw_border_with_background {
resolved.draw_border_with_background = Some(x);
}
if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
}
fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
// Must be ensured by the caller.
let server_pending = role.server_pending.as_ref().unwrap();
if let Some(is_focused) = m.is_focused {
if window.is_focused() != is_focused {
return false;
}
}
if let Some(is_active) = m.is_active {
// Our "is-active" definition corresponds to the window having a pending Activated state.
let pending_activated = server_pending
.states
.contains(xdg_toplevel::State::Activated);
if is_active != pending_activated {
return false;
}
}
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}
+66
View File
@@ -0,0 +1,66 @@
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::wayland::shell::xdg::ToplevelSurface;
use super::ResolvedWindowRules;
use crate::layout::workspace::ColumnWidth;
#[derive(Debug)]
pub struct Unmapped {
pub window: Window,
pub state: InitialConfigureState,
}
#[derive(Debug)]
pub enum InitialConfigureState {
/// The window has not been initially configured yet.
NotConfigured {
/// Whether the window requested to be fullscreened, and the requested output, if any.
wants_fullscreen: Option<Option<Output>>,
},
/// The window has been configured.
Configured {
/// Up-to-date rules.
///
/// We start tracking window rules when sending the initial configure, since they don't
/// affect anything before that.
rules: ResolvedWindowRules,
/// Resolved default width for this window.
///
/// `None` means that the window will pick its own width.
width: Option<ColumnWidth>,
/// Whether the window should open full-width.
is_full_width: bool,
/// Output to open this window on.
///
/// This can be `None` in cases like:
///
/// - There are no outputs connected.
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
/// should fetch the parent's current output again upon mapping.
output: Option<Output>,
},
}
impl Unmapped {
/// Wraps a newly created window that hasn't been initially configured yet.
pub fn new(window: Window) -> Self {
Self {
window,
state: InitialConfigureState::NotConfigured {
wants_fullscreen: None,
},
}
}
pub fn needs_initial_configure(&self) -> bool {
matches!(self.state, InitialConfigureState::NotConfigured { .. })
}
pub fn toplevel(&self) -> &ToplevelSurface {
self.window.toplevel().expect("no X11 support")
}
}
+30
View File
@@ -0,0 +1,30 @@
### VSCode
There seems to be a bug in VSCode's Wayland backend until 1.86.0 which causes the window to not show up when using server-side decorations. So, to run VSCode:
1. Make sure VSCode is 1.86.0 or above, or that `prefer-no-csd` is **not set** in the niri config
2. Run `code --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations`
Also, if you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode. That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance. Apparently, VSCode currently unconditionally queries the X server for a keymap.
### Chromium
When creating new windows within Chromium (e.g. with <kbd>Ctrl</kbd><kbd>N</kbd>), there's a Chromium bug with sizing:
- With CSD (`prefer-no-csd` unset), the window will be a bit smaller than needed
- With SSD (`prefer-no-csd` set), the window buffer will be offset to the top-left
Both of these can be fixed by resizing the new Chromium window.
### WezTerm
There's [a bug](https://github.com/wez/wezterm/issues/4708) in WezTerm that it waits for a zero-sized Wayland configure event, so its window never shows up in niri. To work around it, put this window rule in the niri config (included in the default config):
```
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
```
This empty default column width lets WezTerm pick its own initial width which makes it show up properly.
+258
View File
@@ -0,0 +1,258 @@
### Overview
Niri has several animations which you can configure in the same way.
Additionally, you can disable or slow down all animations at once.
Here's a quick glance at the available animations with their default values.
```
animations {
// Uncomment to turn off all animations.
// You can also put "off" into each individual animation to disable it.
// off
// Slow down all animations by this factor. Values below 1 speed them up instead.
// slowdown 3.0
// Individual animations.
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
window-close {
duration-ms 150
curve "ease-out-quad"
}
horizontal-view-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
window-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
window-resize {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
```
### Animation Types
There are two animation types: easing and spring.
Each animation can be either an easing or a spring.
#### Easing
This is a relatively common animation type that changes the value over a set duration using an interpolation curve.
To use this animation, set the following parameters:
- `duration-ms`: duration of the animation in milliseconds.
- `curve`: the easing curve to use.
```
animations {
window-open {
duration-ms 150
curve "ease-out-expo"
}
}
```
Currently, niri only supports three curves:
- `ease-out-quad` <sup>Since: 0.1.5</sup>
- `ease-out-cubic`
- `ease-out-expo`
You can get a feel for them on pages like [easings.net](https://easings.net/).
#### Spring
Spring animations use a model of a physical spring to animate the value.
They notably feel better with touchpad gestures, because they take into account the velocity of your fingers as you release the swipe.
Springs can also oscillate / bounce at the end with the right parameters if you like that sort of thing, but they don't have to (and by default they mostly don't).
Due to springs using a physical model, the animation parameters are less obvious and generally should be tuned with trial and error.
Notably, you cannot directly set the duration.
You can use the [Elastic](https://flathub.org/apps/app.drey.Elastic) app to help visualize how the spring parameters change the animation.
A spring animation is configured like this, with three mandatory parameters:
```
animations {
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
}
```
The `damping-ratio` goes from 0.1 to 10.0 and has the following properties:
- below 1.0: underdamped spring, will oscillate in the end.
- above 1.0: overdamped spring, won't oscillate.
- 1.0: critically damped spring, comes to rest in minimum possible time without oscillations.
However, even with damping ratio = 1.0, the spring animation may oscillate if "launched" with enough velocity from a touchpad swipe.
> [!WARNING]
> Overdamped springs currently have some numerical stability issues and may cause graphical glitches.
> Therefore, setting `damping-ratio` above `1.0` is not recommended.
Lower `stiffness` will result in a slower animation more prone to oscillation.
Set `epsilon` to a lower value if the animation "jumps" at the end.
> [!TIP]
> The spring *mass* (which you can see in Elastic) is hardcoded to 1.0 and cannot be changed.
> Instead, change `stiffness` proportionally.
> E.g. increasing mass by 2× is the same as decreasing stiffness by 2×.
### Animations
Now let's go into more detail on the animations that you can configure.
#### `workspace-switch`
Animation when switching workspaces up and down, including after the vertical touchpad gesture (a spring is recommended).
```
animations {
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
}
```
#### `window-open`
Window opening animation.
This one uses an easing type by default.
```
animations {
window-open {
duration-ms 150
curve "ease-out-expo"
}
}
```
#### `window-close`
<sup>Since: 0.1.5</sup>
Window closing animation.
This one uses an easing type by default.
```
animations {
window-open {
duration-ms 150
curve "ease-out-quad"
}
}
```
#### `horizontal-view-movement`
All horizontal camera view movement animations, such as:
- 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.
- After a horizontal touchpad gesture (a spring is recommended).
```
animations {
horizontal-view-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
#### `window-movement`
<sup>Since: 0.1.5</sup>
Movement of individual windows within a workspace.
Includes:
- Moving window columns with `move-column-left` and `move-column-right`.
- Moving windows inside a column with `move-window-up` and `move-window-down`.
- Moving windows out of the way upon window opening and closing.
- Window movement between columns when consuming/expelling.
This animation *does not* include the camera view movement, such as scrolling the workspace left and right.
```
animations {
window-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
#### `window-resize`
<sup>Since: 0.1.5</sup>
Window resize animation.
Only manual window resizes are animated, i.e. when you resize the window with `switch-preset-column-width` or `maximize-column`.
Also, very small resizes (up to 10 pixels) are not animated.
```
animations {
window-resize {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
#### `config-notification-open-close`
The open/close animation of the config parse error and new default config notifications.
This one uses an underdamped spring by default (`damping-ratio=0.6`) which causes a slight oscillation in the end.
```
animations {
config-notification-open-close {
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
```
### Synchronized Animations
<sup>Since: 0.1.5</sup>
Sometimes, when two animations are meant to play together synchronized, niri will drive them both with the same configuration.
For example, if a window resize causes the view to move, then that view movement animation will also use the `window-resize` configuration (rather than the `horizontal-view-movement` configuration).
This is especially important for animated resizes to look good when using `center-focused-column "always"`.
As another example, resizing a window in a column vertically causes other windows to move up or down into their new position.
This movement will use the `window-resize` configuration, rather than the `window-movement` configuration, to keep the animations synchronized.
A few actions are still missing this synchronization logic, since in some cases it is difficult to implement properly.
Therefore, for the best results, consider using the same parameters for related animations (they are all the same by default):
- `horizontal-view-movement`
- `window-movement`
- `window-resize`
+140
View File
@@ -0,0 +1,140 @@
### Overview
Niri has several options that are only useful for debugging, or are experimental and have known issues.
They are not meant for normal use.
> [!CAUTION]
> These options are **not** covered by the [config breaking change policy](./Configuration:-Overview.md).
> They can change or stop working at any point with little notice.
Here are all the options at a glance:
```
debug {
preview-render "screencast"
// preview-render "screen-capture"
enable-overlay-planes
disable-cursor-plane
render-drm-device "/dev/dri/renderD129"
dbus-interfaces-in-non-session-instances
wait-for-frame-completion-before-queueing
emulate-zero-presentation-time
enable-color-transformations-capability
}
```
### `preview-render`
Make niri render the monitors the same way as for a screencast or a screen capture.
Useful for previewing the `block-out-from` window rule.
```
debug {
preview-render "screencast"
// preview-render "screen-capture"
}
```
### `enable-overlay-planes`
Enable direct scanout into overlay planes.
May cause frame drops during some animations on some hardware (which is why it is not the default).
Direct scanout into the primary plane is always enabled.
```
debug {
enable-overlay-planes
}
```
### `disable-cursor-plane`
Disable the use of the cursor plane.
The cursor will be rendered together with the rest of the frame.
Useful to work around driver bugs on specific hardware.
```
debug {
disable-cursor-plane
}
```
### `render-drm-device`
Override the DRM device that niri will use for all rendering.
You can set this to make niri use a different primary GPU than the default one.
```
debug {
render-drm-device "/dev/dri/renderD129"
}
```
### `dbus-interfaces-in-non-session-instances`
Make niri create its D-Bus interfaces even if it's not running as a `--session`.
Useful for testing screencasting changes without having to relogin.
The main niri instance will *not* currently take back the interfaces when you close the test instance, so you will need to relogin in the end to make screencasting work again.
```
debug {
dbus-interfaces-in-non-session-instances
}
```
### `wait-for-frame-completion-before-queueing`
Wait until every frame is done rendering before handing it over to DRM.
Useful for diagnosing certain synchronization and performance problems.
```
debug {
wait-for-frame-completion-before-queueing
}
```
### `emulate-zero-presentation-time`
Emulate zero (unknown) presentation time returned from DRM.
This is a thing on NVIDIA proprietary drivers, so this flag can be used to test that niri doesn't break too hard on those systems.
```
debug {
emulate-zero-presentation-time
}
```
### `enable-color-transformations-capability`
Enable the color-transformations capability of the Smithay renderer.
May cause a slight decrease in rendering performance.
Currently, should cause no visible changes in behavior, but it will be needed for HDR support whenever that happens.
So, this flag exists to be able to make sure that nothing breaks.
```
debug {
enable-color-transformations-capability
}
```
### `toggle-debug-tint` Key Binding
This one is not a debug option, but rather a key binding.
It will tint all surfaces green, unless they are being directly scanned out.
It's therefore useful to check if direct scanout is working.
```
binds {
Mod+Shift+Ctrl+T { toggle-debug-tint; }
}
```
+197
View File
@@ -0,0 +1,197 @@
### Overview
In this section you can configure input devices like keyboard and mouse, and some input-related options.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
Settings in those sections will apply to every device of that type.
Currently, there's no way to configure specific devices individually (but that is planned).
All settings at a glance:
```
input {
keyboard {
xkb {
// layout "us"
// variant "colemak_dh_ortho"
// options "compose:ralt,ctrl:nocaps"
// model ""
// rules ""
}
// repeat-delay 600
// repeat-rate 25
// track-layout "global"
}
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
// click-method "clickfinger"
}
mouse {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
trackpoint {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
tablet {
map-to-output "eDP-1"
}
touch {
map-to-output "eDP-1"
}
// disable-power-key-handling
// warp-mouse-to-focus
// focus-follows-mouse
// workspace-auto-back-and-forth
}
```
### Keyboard
#### Layout
In the `xkb` section, you can set layout, variant, options, model and rules.
These are passed directly to libxkbcommon, which is also used by most other Wayland compositors.
See the `xkeyboard-config(7)` manual for more information.
```
input {
keyboard {
xkb {
layout "us"
variant "colemak_dh_ortho"
options "compose:ralt,ctrl:nocaps"
}
}
}
```
When using multiple layouts, niri can remember the current layout globally (the default) or per-window.
You can control this with the `track-layout` option.
- `global`: layout change is global for all windows.
- `window`: layout is tracked for each window individually.
```
input {
keyboard {
track-layout "global"
}
}
```
#### Repeat
Delay is in milliseconds before the keyboard repeat starts.
Rate is in characters per second.
```
input {
keyboard {
repeat-delay 600
repeat-rate 25
}
}
```
### Pointing Devices
Most settings for the pointing devices are passed directly to libinput.
Other Wayland compositors also use libinput, so it's likely you will find the same settings there.
For flags like `tap`, omit them or comment them out to disable the setting.
A few settings are common between `touchpad`, `mouse` and `trackpoint`:
- `natural-scroll`: if set, inverts the scrolling direction.
- `accel-speed`: pointer acceleration speed, valid values are from `-1.0` to `1.0` where the default is `0.0`.
- `accel-profile`: can be `adaptive` (the default) or `flat` (disables pointer acceleration).
Settings specific to `touchpad`s:
- `tap`: tap-to-click.
- `dwt`: disable-when-typing.
- `dwtp`: disable-when-trackpointing.
- `tap-button-map`: can be `left-right-middle` or `left-middle-right`, controls which button corresponds to a two-finger tap and a three-finger tap.
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
Tablets and touchscreens are absolute pointing devices that can be mapped to a specific output like so:
```
input {
tablet {
map-to-output "eDP-1"
}
touch {
map-to-output "eDP-1"
}
}
```
Valid output names are the same as the ones used for output configuration.
### General Settings
These settings are not specific to a particular input device.
#### `disable-power-key-handling`
By default, niri will take over the power button to make it sleep instead of power off.
Set this if you would like to configure the power button elsewhere (i.e. `logind.conf`).
```
input {
disable-power-key-handling
}
```
#### `warp-mouse-to-focus`
Makes the mouse warp to newly focused windows.
X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
```
input {
warp-mouse-to-focus
}
```
#### `focus-follows-mouse`
Focuses windows and outputs automatically when moving the mouse over them.
```
input {
focus-follows-mouse
}
```
#### `workspace-auto-back-and-forth`
Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace).
If this flag is enabled, switching to the same workspace by index twice will switch back to the previous workspace.
Niri will correctly switch to the workspace you came from, even if workspaces were reordered in the meantime.
```
input {
workspace-auto-back-and-forth
}
```

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