Compare commits

...

713 Commits

Author SHA1 Message Date
Ivan Molodetskikh 9063a5dbdc spec: Add mesa-libEGL dependency
Closes https://github.com/YaLTeR/niri/issues/554
2024-08-10 14:55:56 +03:00
Ivan Molodetskikh 892e848985 Update README 2024-08-10 12:55:47 +03:00
Ivan Molodetskikh 0edb90bab2 README: Add similar projects 2024-08-10 12:55:38 +03:00
Ivan Molodetskikh 8f71f8958e Bump version to 0.1.8 2024-08-10 12:55:24 +03:00
Ivan Molodetskikh fcb97cfd5e Update dependencies (Smithay Xwayland Nvidia freeze fix) 2024-08-09 19:58:07 +03:00
Ivan Molodetskikh 2983eb3113 wiki: Bump xwl-satellite higher up 2024-08-08 15:26:06 +03:00
Ivan Molodetskikh a968b1abc0 Fix redundant cast after upgrading csscolorparser 2024-08-08 15:12:48 +03:00
Ivan Molodetskikh 47c964d6fb Upgrade dependencies 2024-08-08 15:06:55 +03:00
Michael Yang 22cb657ef1 fix: change precision to highp 2024-08-08 15:06:23 +03:00
Ivan Molodetskikh bb15d1e850 screencopy: Change integer to fractional scale
That *was* wrong after all.
2024-08-08 13:54:28 +03:00
Ivan Molodetskikh 47680e43c5 screencopy: Wait for SyncPoint before submitting 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 0f1e44aac6 screencopy: Fix transformed damage calculation 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 66aae91bca screencopy: Clarify the use of integer scale 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 07bd76e219 screencopy: Use monotonic time
This way it matches up with presentation-time.
2024-08-08 13:32:37 +03:00
Michael Yang b6a7b3e9e4 feat: update screencopy to version 3 2024-08-08 13:32:37 +03:00
Ivan Molodetskikh 1cf5cfce06 Bump MSRV to 1.77.0
New pipewire-rs requires it.
2024-08-06 18:17:43 +03:00
Ivan Molodetskikh 8ff90c4fc2 Implement PipeWire DMA-BUF modifier negotiation 2024-08-06 18:01:52 +03:00
Ivan Molodetskikh 908c8eb42a wiki: Use HTML dark/light image
Apparently GitHub Markdown is not supported on GitHub Wiki.
2024-08-01 18:26:17 +03:00
Ivan Molodetskikh 0078293d4c wiki: Document the redraw loop 2024-08-01 17:52:34 +03:00
Jeff Peeler 9728dbeeac add mod3 key binding support (#565)
* add support for iso_level5_shift modifier

* update Cargo.lock

bumps smithay to de94e8f59e202b605c35dfe1fef1857bad427e8c
2024-07-31 15:00:35 +00:00
Ivan Molodetskikh 324029ca3b Deal with Clippy warnings 2024-07-28 11:41:09 +03:00
Ivan Molodetskikh 73be5b2ba1 CI: Switch leftover action to dtolnay/rust-toolchain
Missed this I guess.
2024-07-28 11:04:02 +03:00
Ivan Molodetskikh af904d23ac tty: Add check for vblank on idle 2024-07-27 13:43:27 +03:00
Ivan Molodetskikh ad84fc1479 wiki: Fix em-dash 2024-07-27 10:14:06 +03:00
Ivan Molodetskikh d5a8074b53 Add profile-with-tracy-ondemand feature
Finally this can be added without disabling frames.

manual-lifetime is needed to avoid initializing Tracy for CLI commands,
since that is quite slow.
2024-07-27 09:51:44 +03:00
Ivan Molodetskikh c506fecc87 Upgrade dependencies 2024-07-27 09:28:40 +03:00
Ivan Molodetskikh d777810911 pw: Don't require LINEAR buffer
It's not needed and apparently doesn't work on NVIDIA together with the
rendering flag.
2024-07-26 16:06:33 +03:00
Ivan Molodetskikh bbdc07ee6c wiki: Document output background-color 2024-07-26 11:51:29 +03:00
Anant Sharma 689338f059 Add background color option for output 2024-07-26 11:51:29 +03:00
Ivan Molodetskikh eee770514f wiki: Mention nightly COPR 2024-07-22 13:49:43 +03:00
Ivan Molodetskikh 5a0bda7ec4 wiki: Document negative struts 2024-07-22 13:12:42 +03:00
Ivan Molodetskikh b454fd5d9e Add negative struts to tests 2024-07-22 13:12:42 +03:00
Salman Farooq 2a830ed498 feat: negative struts (to remove outer gaps) 2024-07-22 13:12:42 +03:00
Ivan Molodetskikh e98d1ec5a7 Add an rpkg spec template 2024-07-17 22:08:15 +03:00
Ivan Molodetskikh 3ace97660f Implement gradient color interpolation option (#548)
* Added the better color averaging code (tested & functional)

* rustfmt

* Make Color f32 0..1, clarify premul/unpremul

* Fix imports and test name

* Premultiply gradient colors matching CSS

* Fix indentation

* fixup

* Add gradient image

---------

Co-authored-by: K's Thinkpad <K.T.Kraft@protonmail.com>
2024-07-16 07:22:03 +00:00
Ivan Molodetskikh 0824737757 border: Fix reversed gradient at angle = 90 2024-07-13 19:02:04 +03:00
Ivan Molodetskikh 8fdea033bc Fix Clippy warnings 2024-07-13 07:48:07 +03:00
Ivan Molodetskikh 2e906fc5fa Add middle-emulation libinput flag 2024-07-13 07:34:22 +03:00
Tglman a5a34934df feat: add metadata for generate deb package with cargo deb 2024-07-12 16:58:30 +03:00
Ivan Molodetskikh 08a8a0f29a Update Cargo.lock 2024-07-12 10:44:02 +03:00
Oli Strik 519611c6c8 Add schemars::JsonSchema trait to ipc types (#536)
* feat: add schemars JsonSchema trait to ipc types

* niri-ipc: use feature-flag for deriving schemars::JsonSchema

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-12 05:21:52 +00:00
Winter a283c34dbb Add move-column-{left/right}-or-to-monitor-{left/right} (#528)
* feature added, move-column-left-or-monitor-left and move-column-right-or-monitor-right

* fixed stupid mistake

* yalter's fixes

* fixed names

* fixed a stupid mistake

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-10 04:52:48 +00:00
Ivan Molodetskikh f9fe86ee3e Restore VRR on TTY switch 2024-07-09 14:25:02 +04:00
Ivan Molodetskikh 2e67152941 Fix view offset anim restart on switching focus 2024-07-09 09:50:46 +04:00
Ivan Molodetskikh 22bfec7259 Add tolerance to view offset anim restart check
It was getting tripped by tiny differences.
2024-07-09 09:43:43 +04:00
Suyashtnt 1af9f9bd95 niri-config: update wiki parses test to test all codeblocks
This makes sure the failing codeblocks do fail. This also optimizes the algorithm a bit by removing a `.collect()`

Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 19:43:07 +03:00
Suyashtnt 926451c8be wiki: update no-test comments in wiki
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 19:43:07 +03:00
Suyashtnt 7b3bef124d niri-config: add test to see if all snippets inside of the wiki compile
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Suyashtnt 3be6e38af3 wiki: update wiki kdl snippets
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Suyashtnt f2290a43d9 flake: update nix flake
Signed-off-by: Suyashtnt <suyashtnt@gmail.com>
2024-07-08 17:42:09 +03:00
Ivan Molodetskikh 4513663084 screenshot-ui: Animate opening 2024-07-08 11:24:08 +04:00
Ivan Molodetskikh 092cf6cfaf solid_color: Fix alpha handling
It wasn't getting redrawn on alpha changes.
2024-07-08 11:11:06 +04:00
Ivan Molodetskikh 236f96e676 screenshot-ui: Add a help panel 2024-07-08 10:54:21 +04:00
Ivan Molodetskikh 887ca971ab Use is_alive() 2024-07-08 10:06:06 +04:00
Ivan Molodetskikh 4cc195b681 screenshot-ui: Pre-compute PrimaryGpuTexture 2024-07-08 10:04:43 +04:00
Ivan Molodetskikh fc2be2b8d0 Upgrade dependencies 2024-07-08 09:38:18 +04:00
Christian Meissl 570bf1cb3c bump smithay 2024-07-08 08:30:00 +03:00
Ivan Molodetskikh 6ec9c72539 Clear pointer grab upon opening the screenshot UI
Gets rid of DND surfaces.
2024-07-07 09:54:19 +04:00
Ivan Molodetskikh 1a1086206c Extract capture_screenshots() 2024-07-07 09:48:19 +04:00
Ivan Molodetskikh f2766b103d Implement toggling pointer for the screenshot UI 2024-07-07 09:23:59 +04:00
Ivan Molodetskikh 62c9d44b04 screenshot-ui: Fix last selection preservation
Another missed thing from the fractional scale refactor...
2024-07-07 09:22:39 +04:00
Ivan Molodetskikh e394a7ff20 Implement on-demand layer-shell keyboard focus 2024-07-06 18:20:19 +04:00
Ivan Molodetskikh 921ed63204 Add LayerSurface to PointerFocus 2024-07-06 18:17:48 +04:00
Ivan Molodetskikh 77dafb819f Fix screenshot UI selection pointer clamping 2024-07-06 09:46:37 +04:00
Ivan Molodetskikh 1da99f4003 Implement focus-follows-mouse max-scroll-amount 2024-07-05 20:53:11 +04:00
Ivan Molodetskikh 120eaa6c56 wiki: Fix repeat since annotation 2024-07-05 20:30:27 +04:00
Ivan Molodetskikh fb636ef98d Refactor and simplify new view offset calculation
* Split new offset computation from starting the animation.
* Simplify new column on empty workspace logic.
2024-07-05 20:30:27 +04:00
Ivan Molodetskikh 6147a31b48 wiki: Add Since to repeat=false 2024-07-05 12:04:23 +04:00
Ivan Molodetskikh 3f8707496f layout: Remove todo!() when activating window with no monitors 2024-07-05 11:56:45 +04:00
Ivan Molodetskikh de6caec685 Recompute current pointer focus for focus-follows-mouse
Fixes https://github.com/YaLTeR/niri/issues/377.
2024-07-05 10:13:50 +04:00
Ivan Molodetskikh c8411e55d9 wiki: Mention bind key repeat 2024-07-05 08:40:25 +03:00
Salman Farooq d3aebdbec4 Implement key repeat for compositor binds 2024-07-05 08:40:25 +03:00
TheAngusMcFire a56e4ff436 Added Commnads to focus windows or Monitors above/below the active window (#497)
* Implement focus-window-up/down-or-monitor calls

* Fixed wrong naming of focus-window-or-monitor commands

* fix copy pase errors for focusing direction

* Fixed wrong behaviour when the current workspace is empty

* Cleanup navigation code to reduce complexity

* Fix wrong comments and add testcases for FocusWindowOrMonitorUp/Down

---------

Co-authored-by: Christian Rieger <christian.rieger@student.tugraz.at>
2024-07-05 04:55:04 +00:00
Ivan Molodetskikh 9dcc9160b3 Put Outputs config into a dedicated struct 2024-07-05 07:35:01 +03:00
tet 43df7fad46 Implement wlr-output-management protocol
fix: wlr_output_management use WeakOutput
2024-07-05 07:35:01 +03:00
Ivan Molodetskikh d2087a2cd9 Add output ID tracking 2024-07-05 07:35:01 +03:00
Nick Hastings c681198179 Add install location instructions for manual installation (#489)
* wiki: Update install location instructions

Provide file install destinations for both packages and manual
installations.

* wiki: split install instructions into two sections

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

* Update wiki/Getting-Started.md

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-02 08:30:39 +00:00
it-a-me 105938df0b Keep monitors powered off upon connecting a new one (#488)
* Keep monitors powered off upon connecting a new one

Update src/backend/tty.rs

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

Update src/backend/tty.rs

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

fix tests

* Update

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-07-02 01:21:07 -07:00
Ivan Molodetskikh 7b6fa12854 Enable subpixel glyph positioning in Pango
Makes things scale more smoothly.
2024-07-01 09:47:31 +04:00
Ivan Molodetskikh e7c201abba Update README 2024-06-29 10:27:38 +04:00
Ivan Molodetskikh 4fd04951e6 Bump version to 0.1.7 2024-06-29 08:39:13 +04:00
Salman Farooq 747c186293 add-in-wiki-xwayland-run-as-a-solution-to-run-X-apps (#477) 2024-06-28 21:18:29 -07:00
Filipe Paniguel bdf9894020 feat: add focus-column-or-monitor-left, focus-column-or-monitor-right (#456)
* feat: add support for focus-window-or-monitor

* addresses output without window case

* refactor: reduce verbosity

* update this..

* refactor: rename `maybe_focus_window` functions

* refactor: flip focus_window_or_output return logic

* Update src/layout/mod.rs

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

* refactor: rename to Column

* move blocks next to other Column variables

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-28 07:44:24 -07:00
sodiboo d180e60e05 Implement support for $NIRI_CONFIG environment variable 2024-06-28 14:00:26 +03:00
sodiboo 65addefd09 wiki: Fix $XDG_CONFIG_HOME/.config/ that should be $XDG_CONFIG_HOME/ 2024-06-28 14:00:26 +03:00
Ivan Molodetskikh 697fcbac12 wiki: Add rounded corners to the FAQ 2024-06-28 14:39:04 +04:00
Ivan Molodetskikh a8e281e95f wiki: Fix links 2024-06-28 14:38:58 +04:00
Ivan Molodetskikh 4d60eae82e Fix blocked-out + popups and rounded corners window screencasts 2024-06-28 12:35:12 +04:00
Ivan Molodetskikh 2b5215c244 Show ISO_Level3_Shift in the hotkey overlay 2024-06-28 11:28:40 +04:00
Ivan Molodetskikh a43f30b7f5 Ignore compositor opacity for window screencasts
When using opacity as unfocused indicator, it will show up on the
screencast, which is undesired.

This is not a problem for window screen*shot*s where the window is
focused.
2024-06-28 10:39:36 +04:00
Ivan Molodetskikh 88f7b08e56 Add transparency support to window screencasts
Turns out it needed to be in a separate pod.
2024-06-28 10:39:35 +04:00
Ivan Molodetskikh dc92d80b9f Implement initial window screencasting 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 0757ad08e7 id: Start from 1 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 5577021475 wiki: Mention wait for completion NVIDIA flickering workaround 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 40aff3a094 Implement org/gnome/shell/Introspect/GetWindows 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 6c5f10035a mapped: Add id 2024-06-28 10:39:35 +04:00
Ivan Molodetskikh 96d2baa2b5 mapped: Make is_active_in_column private 2024-06-28 10:39:35 +04:00
aspizu 5d2754f831 Fix dead links and add FAQ entry (#475)
* Fix dead links and add FAQ entry

* Update wiki/FAQ.md

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

* Update wiki/Important-Software.md

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

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-27 23:23:52 -07:00
itsjunetime ebaf1b0620 Update winit to fix failing build on arm linux 2024-06-22 18:21:15 +03:00
Ivan Molodetskikh 589e5a600c Keep screencast running through size changes 2024-06-21 11:05:28 +03:00
Ivan Molodetskikh 198b5a502d Update dependencies 2024-06-21 08:55:46 +03:00
Ivan Molodetskikh cb0ebd35ce Make tablet without specific output map to union of outputs 2024-06-19 23:02:45 +03:00
Ivan Molodetskikh 29cf80a3dd wiki: Mention workspace switch mouse gesture 2024-06-19 22:22:34 +03:00
Ivan Molodetskikh db89d4d3dd Implement vertical middle mouse gesture 2024-06-19 21:55:39 +03:00
Kirill Chibisov 226273f660 Handle KDE decorations in Mapped::has_ssd
This fixes an issue with default CSD border being drawn for SSD
rendering firefox, because only xdg decorations were checked.
2024-06-19 17:42:29 +03:00
Ivan Molodetskikh c0ded35783 Somewhat fix height distribution logic
This got a bit broken with fractional layout. The current logic seems to
give exact results for integer scales again, but for fractional scales
sometimes the resulting height goes beyond the maximum, even clearly by
more than one logical pixel. Not entirely sure why that is.
2024-06-19 08:51:19 +03:00
FreeFull 39632e9c1e Add regex syntax link to Configuration:-Window-Rules.md 2024-06-18 14:31:57 +03:00
Ivan Molodetskikh 66202992c9 Fix blurry rounded corners on high scales 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh eb59b10050 config: Remove obsolete FIXME 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 986f2c14ab Make scale use FloatOrInt 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 793e1bdbc5 Animate xdg-activation and foreign-toplevel workspace switches
These are a bit jarring without an animation.
2024-06-18 14:01:34 +03:00
Ivan Molodetskikh d62721d5f8 Queue redraw after activation in xdg-activation 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh d54619e1d1 Remove unnecessary return 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 8425493ef5 Allow scale below 1 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 6121e64338 Add fractional scales to auto scale guessing 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 33b5beaeee Round scale to closest representable 2024-06-18 14:01:34 +03:00
Ivan Molodetskikh 1dae45c58d Refactor layout to fractional-logical
Lets borders, gaps, and everything else stay pixel-perfect even with
fractional scale. Allows setting fractional border widths, gaps,
struts.

See the new wiki .md for more details.
2024-06-18 14:01:28 +03:00
Ivan Molodetskikh 997119c443 Enable fractional scaling 2024-06-18 12:23:50 +03:00
Ivan Molodetskikh 032589446a Fix cached data not updating on config change 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 9ae98e09cb Update Smithay 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 2ffa1ae705 layout: Cache scale and transform on the workspace 2024-06-17 09:02:22 +03:00
Ivan Molodetskikh fee72b87cf niri-config: Add pretty-assertions to tests
The config parse test is pretty big and it's impossible to tell the
difference from the normal assert.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 6c47bd6e80 Rename apply_scale to to_physical_precise_round
Consistency with Smithay.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 02c2972e74 ui/config_error_notification: Store TextureBuffers
Avoids re-importing every frame.
2024-06-17 09:02:22 +03:00
Ivan Molodetskikh 4b830ee7ff ui/screenshot_ui: Correct fractional scaled behavior 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 8e41568ffd Add SolidColor{Buffer,RenderElement} 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh dbe810d3d8 Move apply_scale() to utils 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh a1563b9132 ui/config_error_notification: Make fractional-scaling aware 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 98aea9579f ui/exit_confirm_dialog: Make fractional-scaling aware 2024-06-10 18:08:01 +03:00
Ivan Molodetskikh 7019172b67 Add MemoryBuffer 2024-06-10 18:08:00 +03:00
Ivan Molodetskikh be62bd123a ui/hotkey_overlay: Make fractional-scaling aware 2024-06-10 18:08:00 +03:00
Ivan Molodetskikh 3c63be6261 Implement our own TextureBuffer/RenderElement
Supports fractional texture scale + has some getters.
2024-06-10 18:08:00 +03:00
Ivan Molodetskikh e3406ac255 Signal fractional scale to clients
Doesn't do anything yet because we don't bind the fractional scale
manager and don't allow fractional scales.
2024-06-10 18:08:00 +03:00
Ivan Molodetskikh 22a948cc75 Update dependencies 2024-06-10 18:07:51 +03:00
Peter Collingbourne bc3d6cac80 Implement xdg_activation_v1
Fixes #30.
2024-06-10 18:06:34 +03:00
James Sully a55e385b12 Add focus-column-right-or-first, focus-column-left-or-last (#391)
* add focus-column-right-or-first

* add focus-column-left-or-last
2024-06-09 11:14:51 +00:00
Ujp8LfXBJ6wCPR af6d84a7f8 Fix typos (#429)
* Fix typos reported by "typos" crate

https://github.com/crate-ci/typos

* Ignore typo datas -> data

See https://github.com/crate-ci/typos?tab=readme-ov-file#false-positives
for more configureability.

---------

Co-authored-by: Carl Hjerpe <git@hjerpe.xyz>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-06-09 10:50:22 +00:00
Ivan Molodetskikh f203c8729a Use generic Atomic for rlim_t
rlim_t is different between platforms.
2024-06-09 08:48:36 +03:00
galister dbf0dddfcc PointerMotionAbsolute: use union rect of all outputs 2024-06-07 19:50:48 +03:00
Ivan Molodetskikh c6c17cccac Add missing fullscreen check
Fixes crash when a window in a column requests to be unfullscreened.
2024-06-04 19:46:19 +03:00
Ivan Molodetskikh b5ad0e12fd Preserve empty named workspaces upon output removal
Not sure how we missed this.
2024-06-02 08:21:19 +03:00
Yuya Nishihara c8e46b9d17 Add "off" and "disabled-on-external-mouse" properties to input devices
This is called "events <mode>" in Sway, but we decided to use more abstracted
form for consistency with the other config items. "disabled-on-external-mouse"
is added only to touchpads, but there might be other devices that support this
option.

I think "off" also applies to keyboards, but I'm not going to add the one
because we don't have libinput machinery for the keyboard config, and it's
unlikely that user wants to disable _all_ keyboards. OTOH, pointer devices can
be disabled per type. Perhaps, this should be revisited after implementing #371.
2024-05-29 16:41:03 +03:00
Yuya Nishihara f2ce84b243 Fix copy-paste error in scroll-method error message 2024-05-28 15:35:45 +03:00
rustysec ae7fb4c4f4 Add niri msg focused-output 2024-05-26 21:29:22 +04:00
Yuya Nishihara 4746a0da7d Add scroll-method property to pointer devices
My use case is to enable middle-button scroll on my keyboard with pointing
stick. The device is recognized as USB mouse.
2024-05-26 16:49:40 +03:00
Ivan Molodetskikh 2ac8d84034 Update Smithay (NVIDIA 555 fix) 2024-05-24 16:47:55 +04:00
Micah N Gorrell eb0f7aa429 Added actions to allow focusing up or down as normal but to wrap to the column to the left or right if there is no window above or below 2024-05-24 16:44:20 +04:00
Ivan Molodetskikh bcca03cce7 Increase RLIMIT_NOFILE to maximum
Fixes Xwayland + RustRover crashing.

See similar changes:
* https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2235
* https://github.com/swaywm/sway/pull/6629
2024-05-23 09:59:34 +04:00
Ivan Molodetskikh efb39e466b default-config: Clarify spawn comments 2024-05-21 22:33:50 +04:00
Ivan Molodetskikh 14d637f4ef wiki: Mention left-handed 2024-05-21 11:06:52 +04:00
Ivan Molodetskikh c9d90afe59 Add left-handed input property
Closes https://github.com/YaLTeR/niri/issues/366
2024-05-21 10:10:11 +04:00
Ivan Molodetskikh d088ce248f wiki: Mention xwayland-satellite 2024-05-21 08:17:57 +04:00
Ivan Molodetskikh f4cdde1f4f Fix no outputs case handling in a few places 2024-05-20 15:36:08 +04:00
Ivan Molodetskikh 56e02a398d Add Default impl for niri_config::Keyboard
Fixes https://github.com/YaLTeR/niri/issues/357
2024-05-19 17:55:54 +04:00
lpnh 2552b129c4 refactor: make example ready to copy and paste 2024-05-18 20:17:39 +03:00
Ivan Molodetskikh d96a66ddff Update README 2024-05-18 15:00:39 +04:00
Ivan Molodetskikh bfaf9ae060 Bump version to 0.1.6 2024-05-18 14:35:42 +04:00
Ivan Molodetskikh 2da0aaace8 wiki: Update different-corner-radius image 2024-05-18 10:50:11 +04:00
Ivan Molodetskikh ee12bbc9ed wiki: Change two instances of Telegram to Fractal 2024-05-18 09:44:48 +04:00
Ivan Molodetskikh cc4026f588 wiki: Add since to interactive resize 2024-05-18 08:59:11 +04:00
Ivan Molodetskikh aa74120143 wiki: Fix typo 2024-05-18 08:50:17 +04:00
Ivan Molodetskikh 473ef22de2 Redraw on lock surface children commits 2024-05-17 15:59:49 +04:00
Ivan Molodetskikh d76b213e03 Update Smithay (session-lock fix) 2024-05-17 15:49:02 +04:00
Ivan Molodetskikh 4dc7a6ceb8 Rearrange CLI subcommands a bit 2024-05-17 10:33:00 +03:00
rustysec 36d3e70f11 Implement niri msg workspaces 2024-05-17 10:33:00 +03:00
Ivan Molodetskikh a2f74c9bff Update Smithay (buffer leak and crash fix) 2024-05-17 07:54:56 +04:00
Ivan Molodetskikh 0ce08b598c Bump package versions 2024-05-16 18:04:18 +04:00
Ivan Molodetskikh ae63773737 Update Smithay and other deps 2024-05-16 18:00:28 +04:00
Ivan Molodetskikh c5ca412829 wiki: Add since to named workspaces 2024-05-16 13:04:51 +04:00
Ivan Molodetskikh cbfc682f9a Implement at-startup window rule 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh c64d9e5223 Fix missing check in Match PartialEq 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh 4e31f7e047 wiki: Document named workspaces 2024-05-16 01:24:34 -07:00
Ivan Molodetskikh 109d99fe82 Make workspace names case-insensitive 2024-05-16 01:24:34 -07:00
Gergely Nagy eb9bbe3352 Implement named workspaces
This is an implementation of named, pre-declared workspaces. With this
implementation, workspaces can be declared in the configuration file by
name:

```
workspace "name" {
  open-on-output "winit"
}
```

The `open-on-output` property is optional, and can be skipped, in which
case the workspace will open on the primary output.

All actions that were able to target a workspace by index can now target
them by either an index, or a name. In case of the command line, where
we do not have types available, this means that workspace names that
also pass as `u8` cannot be switched to by name, only by index.

Unlike dynamic workspaces, named workspaces do not close when they are
empty, they remain static. Like dynamic workspaces, named workspaces are
bound to a particular output. Switching to a named workspace, or moving
a window or column to one will also switch to, or move the thing in
question to the output of the workspace.

When reloading the configuration, newly added named workspaces will be
created, and removed ones will lose their name. If any such orphaned
workspace was empty, they will be removed. If they weren't, they'll
remain as a dynamic workspace, without a name. Re-declaring a workspace
with the same name later will create a new one.

Additionally, this also implements a `open-on-workspace "<name>"` window
rule. Matching windows will open on the given workspace (or the current
one, if the named workspace does not exist).

Signed-off-by: Gergely Nagy <niri@gergo.csillger.hu>
2024-05-16 01:24:34 -07:00
Ivan Molodetskikh 229ca90507 wiki: Mention where to find shader compile warnings 2024-05-15 22:15:39 +04:00
Ivan Molodetskikh 17a71bd424 wiki: Add expanding circle example to window-open 2024-05-15 21:24:18 +04:00
Ivan Molodetskikh a39aaa312d wiki: Add fall_and_rotate window-close custom shader example 2024-05-15 20:55:16 +04:00
Ivan Molodetskikh 3f802d0193 Clarify surface destroyed comment 2024-05-15 20:30:02 +04:00
Ivan Molodetskikh df36eac25b Fix render elements looking off on screenshots 2024-05-15 20:09:49 +04:00
Ivan Molodetskikh 609b1a02d0 Change resize shader geo size to logical pixels
Consistent with the others.
2024-05-15 19:52:11 +04:00
Ivan Molodetskikh 5335ef454b Implement custom shader for window-open 2024-05-15 19:38:29 +04:00
Ivan Molodetskikh 496cd59df9 Use correct function name in comment 2024-05-15 16:51:43 +04:00
Ivan Molodetskikh 3e385d5c48 Clear fd flags before sending selection 2024-05-15 16:49:46 +04:00
Ivan Molodetskikh b87fba2182 tty: Relax device checks on removal 2024-05-15 08:14:09 +04:00
Ivan Molodetskikh 3d63f5e644 tty: Try harder to find a GBM device 2024-05-15 08:13:56 +04:00
Ivan Molodetskikh 1096f0cf0e wiki: Mention kmsro in getting started 2024-05-15 00:30:20 +04:00
Ivan Molodetskikh 78978219a0 tty: Relax primary render node check 2024-05-14 23:39:22 +04:00
Ivan Molodetskikh 5999ba6a5e Avoid changing the view offset if size didn't change 2024-05-14 23:39:19 +04:00
Ivan Molodetskikh 94a9b48a0f Improve interactive resize end edge cases and animations 2024-05-14 20:41:10 +04:00
Ivan Molodetskikh d776ab7763 Fix interactive resize cancelling
The interactive resize may have ended, but we're still waiting for the
last commit of the respective window. When cancelling, we should cancel
those ones too.
2024-05-14 16:29:03 +04:00
Ivan Molodetskikh 5f40221051 Refactor column and tile offsets, fix a few issues 2024-05-14 15:35:43 +04:00
Ivan Molodetskikh b14405904a Draw closing windows in the right order 2024-05-14 14:52:13 +04:00
Ivan Molodetskikh e06776c5d4 wiki: Expand design principles a bit 2024-05-13 08:35:19 +04:00
Ivan Molodetskikh 55e550262d wiki: Fix custom shader examples 2024-05-12 10:08:06 +04:00
Ivan Molodetskikh e5ccc9332c wiki: Fix shader example links 2024-05-12 10:06:26 +04:00
Ivan Molodetskikh 36a54615ca Add crossfade_or_crop_next resize shader example 2024-05-12 09:56:11 +04:00
Ivan Molodetskikh 9004c83954 Implement custom shader for window-close anim 2024-05-12 09:52:36 +04:00
Ivan Molodetskikh 29c7552852 Add linear animation curve 2024-05-12 09:50:16 +04:00
Ivan Molodetskikh d2ed42a157 closing_window: Pass geo size and view rect 2024-05-12 08:46:02 +04:00
Ivan Molodetskikh 4073f9f522 closing_window: Remove starting_alpha/scale 2024-05-12 08:42:43 +04:00
Ivan Molodetskikh 464441f9eb closing_window: Store textures directly 2024-05-11 17:54:27 +04:00
Ivan Molodetskikh bc29256b9d Implement Mod+MMB view offset gesture 2024-05-11 14:02:37 +04:00
Ivan Molodetskikh beba87354a Group input-related things in a subfolder 2024-05-11 13:21:05 +04:00
Ivan Molodetskikh 078724369d wiki: List debug key binds 2024-05-11 12:56:34 +04:00
Ivan Molodetskikh 75393faca3 wiki: Add a few missing things 2024-05-11 12:55:18 +04:00
Ivan Molodetskikh 22cdd044d3 Reset double click timer on gesture trigger 2024-05-11 11:21:57 +04:00
Ivan Molodetskikh 719270854a Update resize commit unconditionally 2024-05-11 10:59:46 +04:00
Ivan Molodetskikh 8900960e76 Don't pass double-resize-right click to window 2024-05-11 10:52:21 +04:00
TheZoq2 47a8e75fd5 Add is_active_in_column
Add missing ```

Fix tests
2024-05-11 10:42:49 +04:00
Ivan Molodetskikh 6d9cfe2882 Don't start a resize if edges is empty 2024-05-11 10:30:51 +04:00
Ivan Molodetskikh de0ad85711 Set cursor for niri-initiated interactive resize 2024-05-11 10:28:38 +04:00
Ivan Molodetskikh f091e64b12 wiki: Add gestures page 2024-05-11 10:09:49 +04:00
Ivan Molodetskikh e454cd6282 Implement double-resize-click to reset height/toggle full width 2024-05-11 10:02:48 +04:00
Ivan Molodetskikh 1c14a0a2a9 Add a reset-window-height action 2024-05-11 09:33:23 +04:00
Ivan Molodetskikh 2fd9a03bd7 Stop confining the pointer during resize grab 2024-05-11 09:26:49 +04:00
Ivan Molodetskikh b101f9b5f8 Render tiles flush to the right when left-resizing
This really needs a refactor...
2024-05-11 09:00:03 +04:00
Ivan Molodetskikh 34bcc6ea93 Split get resize data from update 2024-05-11 08:26:49 +04:00
Ivan Molodetskikh 9dfa121b8e Implement interactive mouse resizing 2024-05-10 20:23:08 +04:00
Ivan Molodetskikh c4ebb9f58e Start Tracy manual-lifetime after niri msg 2024-05-09 11:08:15 +04:00
Ivan Molodetskikh 38e329aab9 Make async-channel non-optional 2024-05-08 08:57:37 +04:00
Ivan Molodetskikh 95a1a01fdc wiki: Add Since to do-screen-transition 2024-05-08 08:43:01 +04:00
Ivan Molodetskikh c61940c40e ipc: Wait until action is processed before returning 2024-05-08 08:30:49 +04:00
Ivan Molodetskikh ed2b6d3894 Mark screen transition texture transparent 2024-05-08 08:21:15 +04:00
Ivan Molodetskikh 47925948a3 Add trace span to do_screen_transition 2024-05-08 08:21:04 +04:00
Ivan Molodetskikh 5248e53499 Implement do-screen-transition action 2024-05-07 22:19:11 +04:00
Ivan Molodetskikh 9847a652af ipc: Respect --json for msg output 2024-05-05 13:08:29 +04:00
Ivan Molodetskikh 96823eea38 Make output name matching case-insensitive 2024-05-05 12:55:57 +04:00
Ivan Molodetskikh ea59091869 Print message when output was not found 2024-05-05 12:50:18 +04:00
Ivan Molodetskikh 2e4a2e13b1 Make missing scale = automatic selection
That was the intention, but I missed it before.
2024-05-05 12:39:20 +04:00
Ivan Molodetskikh df0ee996ee Don't unwrap client
If Smithay posts an error, client will become None immediately, even
while the surface may still receive events.
2024-05-05 11:14:46 +04:00
Ivan Molodetskikh 65b9c74f62 Implement niri msg output 2024-05-05 10:19:47 +04:00
Ivan Molodetskikh 2dff674470 Don't expand zero radius per corner
So that radii like 8 8 0 0 look properly.
2024-05-05 07:43:21 +04:00
Ivan Molodetskikh 23850e1c60 wiki: Try to fix link 2024-05-04 21:16:43 +04:00
Ivan Molodetskikh 641b44e006 Fix blocked-out surfaces on scaled outputs 2024-05-04 20:13:53 +04:00
Ivan Molodetskikh 1394afaae9 wiki: Mention nixos and nvidia issues in getting started 2024-05-04 16:27:14 +04:00
Ivan Molodetskikh 314ad9d3e5 Fix rounded corners on blocked-out resizes 2024-05-04 11:54:52 +04:00
Ivan Molodetskikh 99eb1227b1 Extract RenderTarget::should_block_out() 2024-05-04 11:51:27 +04:00
Ivan Molodetskikh 79093baeee Extract rules out 2024-05-04 11:45:39 +04:00
Ivan Molodetskikh 7093385b4d Update tile before taking unmap snapshot 2024-05-04 11:37:58 +04:00
Ivan Molodetskikh 3748f6cd6a Fix border/focus ring options not applying right away 2024-05-04 11:11:01 +04:00
Ivan Molodetskikh 73cc0079d6 Split update_render_elements() from advance_animations()
advance_animations() is called from places like input, whereas
update_render_elements() is strictly for rendering.
2024-05-04 11:10:02 +04:00
Ivan Molodetskikh 69aeba2a4d shader_element: Store and set location separately 2024-05-04 09:49:32 +04:00
Ivan Molodetskikh 7aab413048 shader_element: Remove size
It's not actually needed.
2024-05-04 09:15:17 +04:00
Ivan Molodetskikh 74996a2416 Make BorderRenderElement scale-agnostic 2024-05-03 21:49:47 +04:00
Ivan Molodetskikh 8ab50f9d1c shader_element: Store program type instead of shader 2024-05-03 21:23:32 +04:00
Ivan Molodetskikh 5c32031111 shader_element: Make shader optional
The element is long-lived, but the shader itself isn't.
2024-05-03 20:20:36 +04:00
Ivan Molodetskikh 85680a57da Reduce unnecessary damage to borders 2024-05-03 13:46:33 +04:00
Ivan Molodetskikh 1a8d6b1f1d Add a semi-working debug-toggle-damage binding 2024-05-03 10:33:31 +04:00
Ivan Molodetskikh 185f294200 wiki: Mention new debug option 2024-05-03 10:23:48 +04:00
Ivan Molodetskikh c6d64dae7a Add debug-toggle-opaque-regions 2024-05-02 17:52:06 +04:00
Ivan Molodetskikh 5dddc850fc wiki: Clarify getting started 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh 2f42f8ac75 Damage window on corner radius changes 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh 42cef79c69 Implement rounded window corners 2024-05-02 14:27:53 +04:00
Ivan Molodetskikh d86df5025c Add Tracy span to Tile::render_inner 2024-05-01 19:04:47 +04:00
Ivan Molodetskikh 9309b3be61 Split rendering between popups and window surface 2024-05-01 19:04:11 +04:00
Ivan Molodetskikh c5be2dd549 Add Tracy span to Tile::render 2024-05-01 19:00:54 +04:00
Ivan Molodetskikh 365dbacae7 Move unmap snapshot from Mapped to Tile 2024-05-01 19:00:19 +04:00
Ivan Molodetskikh af9caa1d9b wiki: Warn against --all-features in getting started 2024-05-01 12:10:38 +04:00
Michael Forster 68ff36f683 Add libXcursor and libXi to nix flake
In my tests this was necessary to develop Niri using non-NixOS Nix.
Otherwise Niri panics with this error message: called `Result::unwrap()`
on an `Err` value: EventLoopCreation(NotSupported(NotSupportedError)).
2024-04-30 02:19:28 -07:00
Ivan Molodetskikh c0d5001e90 Update Smithay 2024-04-29 14:27:38 +04:00
Ivan Molodetskikh f3ded0c2e6 Move shader get out of ResizeRenderElement::new 2024-04-29 14:27:38 +04:00
Ivan Molodetskikh f43fa55526 Fix fullscreen backdrop rendering below focus ring 2024-04-28 06:48:48 +04:00
Ivan Molodetskikh c1c43c5393 Fix size_curr_geo in resize shader 2024-04-27 13:12:21 +04:00
Ivan Molodetskikh 5899010c96 Extract mat3_uniform 2024-04-27 13:11:25 +04:00
Ivan Molodetskikh 9f3715b731 Add distro to issue template 2024-04-27 07:44:51 +04:00
Ivan Molodetskikh 8d99e3c015 Add disable-direct-scanout debug flag 2024-04-25 22:10:52 +04:00
Ivan Molodetskikh 9df71bcb5d Add fixme comment 2024-04-25 08:48:35 +04:00
Ivan Molodetskikh 04c5b9ad74 Only give keyboard focus to exclusive layer-shell surfaces
Workaround until we properly support on-demand.

See: https://github.com/YaLTeR/niri/issues/308
2024-04-25 08:43:37 +04:00
Ivan Molodetskikh fd6c8c7790 Implement focus-ring window rule 2024-04-24 22:17:53 +04:00
Ivan Molodetskikh 3e598c565e Implement border window rule 2024-04-24 22:01:26 +04:00
Ivan Molodetskikh e261b641ed Filter out the Intel CCS modifiers 2024-04-24 12:26:59 +04:00
Ivan Molodetskikh dc1d2b706c Implement ideal scale factor guessing 2024-04-24 12:26:59 +04:00
Ivan Molodetskikh f9b008163c Fix spelling mistake 2024-04-23 00:09:42 -07:00
Kirill Chibisov 279659ac90 Unconstrain InputMethod's PopupSurface
Make IME popup to be visible inside the parent and not obscure the
text input rectangle region.

Fixes https://github.com/YaLTeR/niri/issues/221
2024-04-23 00:09:42 -07:00
Kirill Chibisov c2d03d82ce Use PopupKind instead of PopupSurface 2024-04-23 00:09:42 -07:00
Ivan Molodetskikh 5299590290 Improve cropping logic in resize shader example
The previous logic failed to the left of the geometry.
2024-04-22 22:37:47 +04:00
Ivan Molodetskikh 1681ed16d9 Change custom-shader to a prelude-epilogue system 2024-04-22 19:05:11 +04:00
Ivan Molodetskikh d4bed70884 Advertise Abgr8888 and Xbgr8888 in shm 2024-04-22 17:47:12 +04:00
Ivan Molodetskikh 49f5402669 Implement window-resize custom-shader 2024-04-21 20:16:54 +04:00
Ivan Molodetskikh 2ecbb3f6f8 Remove obsolete comment 2024-04-21 12:28:49 +04:00
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
179 changed files with 37976 additions and 7249 deletions
+1
View File
@@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text
+24
View File
@@ -0,0 +1,24 @@
---
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 distribution, e.g. Fedora 40 Silverblue -->
* Distro:
<!-- 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)
+112 -29
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.77.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.77.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
@@ -98,11 +153,9 @@ jobs:
with:
show-progress: false
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install nightly --profile minimal --component rustfmt
rustup override set nightly
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Run rustfmt
run: cargo fmt --all -- --check
@@ -119,8 +172,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
+1660 -681
View File
File diff suppressed because it is too large Load Diff
+68 -33
View File
@@ -1,5 +1,8 @@
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.0-beta.1"
version = "0.1.8"
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.86"
bitflags = "2.6.0"
clap = { version = "4.5.14", features = ["derive"] }
serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
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.1", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -35,38 +41,45 @@ 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 = "2.3.1"
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"] }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.16.3", features = ["derive"] }
calloop = { version = "0.14.0", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.8.0"
fastrand = "2.1.0"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.28.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" }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.18.0"
pipewire = { version = "0.7.2", optional = true }
png = "0.17.11"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.13"
sd-notify = "0.4.1"
libc = "0.2.155"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.8", path = "niri-config" }
niri-ipc = { version = "0.1.8", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.2.2"
pango = { version = "0.20.0", features = ["v1_44"] }
pangocairo = "0.20.0"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.13"
portable-atomic = { version = "1.7.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.2"
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 }
url = { version = "2.5.2", optional = true }
xcursor = "0.3.6"
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -80,23 +93,33 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
]
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
approx = "0.5.1"
k9 = "0.12.0"
proptest = "1.5.0"
proptest-derive = "0.5.0"
xshell = "0.2.6"
[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-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 the on-demand Tracy profiler instrumentation.
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
# Enables dinit integration (global environment).
dinit = []
[profile.release]
debug = "line-tables-only"
@@ -108,7 +131,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~beta.1"
version = "0.1.8"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -119,3 +142,15 @@ assets = [
]
[package.metadata.generate-rpm.requires]
alacritty = "*"
fuzzel = "*"
[package.metadata.deb]
depends = "alacritty, fuzzel"
assets = [
["target/release/niri", "usr/bin/", "755"],
["resources/niri-session", "usr/bin/", "755"],
["resources/niri.desktop", "/usr/share/wayland-sessions/", "644"],
["resources/niri-portals.conf", "/usr/share/xdg-desktop-portal/", "644"],
["resources/niri.service", "/usr/lib/systemd/user/", "644"],
["resources/niri-shutdown.target", "/usr/lib/systemd/user/", "644"],
]
+35 -158
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> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
## About
@@ -16,26 +20,36 @@ 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
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- 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 +58,25 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Tile Scrollably Elsewhere
## Building
Here are some other projects which implement a similar workflow:
> [!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.
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [papersway]: scrollable tiling on top of sway/i3.
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## 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
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
+5
View File
@@ -0,0 +1,5 @@
ignore-interior-mutability = [
"smithay::desktop::Window",
"smithay::output::Output",
"wayland_server::backend::ClientId",
]
Generated
+18 -18
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1702918879,
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
"lastModified": 1720226507,
"narHash": "sha256-yHVvNsgrpyNTXZBEokL8uyB2J6gB1wEx0KOJzoeZi1A=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
"rev": "0aed560c5c0a61c9385bddff471a13036203e11c",
"type": "github"
},
"original": {
@@ -28,11 +28,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1701411808,
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
"lastModified": 1719815435,
"narHash": "sha256-K2xFp142onP35jcx7li10xUxNVEVRWjAdY8DSuR7Naw=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
"rev": "ebfe2c639111d7e82972a12711206afaeeda2450",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -62,11 +62,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1701697642,
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -77,11 +77,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1702900294,
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
"lastModified": 1720368505,
"narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
"rev": "ab82a9612aa45284d4adf69ee81871a389669a9e",
"type": "github"
},
"original": {
@@ -103,11 +103,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1701372675,
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
"lastModified": 1719760370,
"narHash": "sha256-fsxAuW6RxKZYjAP3biUC6C4vaYFhDfWv8lp1Tmx3ZCY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
"rev": "ea7fdada6a0940b239ddbde2048a4d7dac1efe1e",
"type": "github"
},
"original": {
+19 -13
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; [
@@ -74,6 +74,9 @@
wayland
mesa
libglvnd # For libEGL
xorg.libXcursor
xorg.libXi
libxkbcommon
];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
@@ -87,9 +90,9 @@
checks.niri = niri;
packages.default = niri;
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
packages = niri.runtimeDependencies;
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} rec {
inherit (niri) LIBCLANG_PATH;
packages = niri.runtimeDependencies ++ niri.nativeBuildInputs ++ niri.buildInputs;
# Force linking to libEGL, which is always dlopen()ed, and to
# libwayland-client, which is always dlopen()ed except by the
@@ -100,6 +103,9 @@
"-lwayland-client"
"-Wl,--pop-state"
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath packages;
PKG_CONFIG_PATH = pkgs.lib.makeLibraryPath packages;
};
}
);
+8 -1
View File
@@ -9,8 +9,15 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
smithay.workspace = true
niri-ipc = { version = "0.1.8", path = "../niri-ipc" }
regex = "1.10.6"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
pretty_assertions = "1.4.0"
miette = { version = "5.10.0", features = ["fancy"] }
+2564 -256
View File
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
use std::fs;
use std::path::PathBuf;
struct KdlCodeBlock {
filename: String,
code: String,
line_number: usize,
must_fail: bool,
}
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
let mut lines = file_contents
.lines()
.map(|line| {
// Removes the > from callouts that might contain ```kdl```
let line = line.trim();
if line.starts_with('>') {
if line.len() == 1 {
""
} else {
&line[2..]
}
} else {
line
}
})
.enumerate();
let mut kdl_code_blocks = vec![];
while let Some((line_number, line)) = lines.next() {
if !line.starts_with("```kdl") {
continue;
}
let mut snippet = String::new();
for (_, line) in lines
.by_ref()
.take_while(|(_, line)| !line.starts_with("```"))
{
snippet.push_str(line);
snippet.push('\n');
}
kdl_code_blocks.push(KdlCodeBlock {
code: snippet,
line_number,
filename: filename.to_string(),
must_fail: line.contains("must-fail"),
});
}
kdl_code_blocks
}
#[test]
fn wiki_docs_parses() {
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
let code_blocks = fs::read_dir(wiki_dir)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
.filter(|file| {
file.path()
.extension()
.map(|ext| ext == "md")
.unwrap_or(false)
})
.flat_map(|file| {
let file_contents = fs::read_to_string(file.path()).unwrap();
let file_path = file.path();
let filename = file_path.to_str().unwrap();
extract_kdl_from_file(&file_contents, filename)
});
let mut errors = vec![];
for KdlCodeBlock {
code,
line_number,
filename,
must_fail,
} in code_blocks
{
if let Err(error) = niri_config::Config::parse(&filename, &code) {
if !must_fail {
errors.push(format!(
"Error parsing wiki KDL code block at {}:{}: {:?}",
filename,
line_number,
miette::Report::new(error)
));
}
} else if must_fail {
errors.push(format!(
"Expected error parsing wiki KDL code block at {}:{}",
filename, line_number
));
}
}
if !errors.is_empty() {
panic!(
"Errors parsing {} wiki KDL code blocks:\n{}",
errors.len(),
errors.join("\n")
);
}
}
+7
View File
@@ -8,4 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "0.8.21", optional = true }
serde.workspace = true
serde_json.workspace = true
[features]
clap = ["dep:clap"]
json-schema = ["dep:schemars"]
+649 -6
View File
@@ -2,30 +2,432 @@
#![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)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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),
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
/// configuration subsequently changes in the config file, these temporary changes will be
/// forgotten.
Output {
/// Output name.
output: String,
/// Configuration to apply.
action: OutputAction,
},
/// Request information about workspaces.
Workspaces,
/// Request information about the focused output.
FocusedOutput,
/// 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)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about workspaces.
Workspaces(Vec<Workspace>),
/// Information about the focused output.
FocusedOutput(Option<Output>),
}
/// Actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
// variants from niri-config should be present here.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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>,
},
/// Do a screen transition.
DoScreenTransition {
/// Delay in milliseconds for the screen to freeze before starting the transition.
#[cfg_attr(feature = "clap", arg(short, long))]
delay_ms: Option<u16>,
},
/// 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 next column to the right, looping if at end.
FocusColumnRightOrFirst,
/// Focus the next column to the left, looping if at start.
FocusColumnLeftOrLast,
/// Focus the window or the monitor above.
FocusWindowOrMonitorUp,
/// Focus the window or the monitor below.
FocusWindowOrMonitorDown,
/// Focus the column or the monitor to the left.
FocusColumnOrMonitorLeft,
/// Focus the column or the monitor to the right.
FocusColumnOrMonitorRight,
/// Focus the window below.
FocusWindowDown,
/// Focus the window above.
FocusWindowUp,
/// Focus the window below or the column to the left.
FocusWindowDownOrColumnLeft,
/// Focus the window below or the column to the right.
FocusWindowDownOrColumnRight,
/// Focus the window above or the column to the left.
FocusWindowUpOrColumnLeft,
/// Focus the window above or the column to the right.
FocusWindowUpOrColumnRight,
/// 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 column to the left or to the monitor to the left.
MoveColumnLeftOrToMonitorLeft,
/// Move the focused column to the right or to the monitor to the right.
MoveColumnRightOrToMonitorRight,
/// 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 reference (index or name).
FocusWorkspace {
/// Reference (index or name) of the workspace to focus.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
},
/// 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 reference (index or name).
MoveWindowToWorkspace {
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
},
/// 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 reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
},
/// 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,
},
/// Reset the height of the focused window back to automatic.
ResetWindowHeight,
/// 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,
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions,
/// Toggle visualization of output damage.
DebugToggleDamage,
}
/// Change in window or column size.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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),
}
/// Workspace reference (index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum WorkspaceReferenceArg {
/// Index of the workspace.
Index(u8),
/// Name of the workspace.
Name(String),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum LayoutSwitchTarget {
/// The next configured layout.
Next,
/// The previous configured layout.
Prev,
}
/// Output actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most thigs 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"))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum OutputAction {
/// Turn off the output.
Off,
/// Turn on the output.
On,
/// Set the output mode.
Mode {
/// Mode to set, or "auto" for automatic selection.
///
/// Run `niri msg outputs` to see the available modes.
#[cfg_attr(feature = "clap", arg())]
mode: ModeToSet,
},
/// Set the output scale.
Scale {
/// Scale factor to set, or "auto" for automatic selection.
#[cfg_attr(feature = "clap", arg())]
scale: ScaleToSet,
},
/// Set the output transform.
Transform {
/// Transform to set, counter-clockwise.
#[cfg_attr(feature = "clap", arg())]
transform: Transform,
},
/// Set the output position.
Position {
/// Position to set, or "auto" for automatic selection.
#[cfg_attr(feature = "clap", command(subcommand))]
position: PositionToSet,
},
/// Toggle variable refresh rate.
Vrr {
/// Whether to enable variable refresh rate.
#[cfg_attr(
feature = "clap",
arg(
value_name = "ON|OFF",
action = clap::ArgAction::Set,
value_parser = clap::builder::BoolishValueParser::new(),
),
)]
enable: bool,
},
}
/// Output mode to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ModeToSet {
/// Niri will pick the mode automatically.
Automatic,
/// Specific mode.
Specific(ConfiguredMode),
}
/// Output mode as set in the config file.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ConfiguredMode {
/// Width in physical pixels.
pub width: u16,
/// Height in physical pixels.
pub height: u16,
/// Refresh rate.
pub refresh: Option<f64>,
}
/// Output scale to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ScaleToSet {
/// Niri will pick the scale automatically.
Automatic,
/// Specific scale.
Specific(f64),
}
/// Output position to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum PositionToSet {
/// Position the output automatically.
#[cfg_attr(feature = "clap", command(name = "auto"))]
Automatic,
/// Set a specific position.
#[cfg_attr(feature = "clap", command(name = "set"))]
Specific(ConfiguredPosition),
}
/// Output position as set in the config file.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ConfiguredPosition {
/// Logical X position.
pub x: i32,
/// Logical Y position.
pub y: i32,
}
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Output {
/// Name of the output.
pub name: String,
@@ -41,10 +443,19 @@ 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.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Mode {
/// Width in physical pixels.
pub width: u16,
@@ -52,4 +463,236 @@ 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, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
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.
#[cfg_attr(feature = "clap", value(name("flipped-90")))]
Flipped90,
/// Flipped vertically.
#[cfg_attr(feature = "clap", value(name("flipped-180")))]
Flipped180,
/// Rotated by 270° and flipped horizontally.
#[cfg_attr(feature = "clap", value(name("flipped-270")))]
Flipped270,
}
/// Toplevel window.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Window {
/// Title, if set.
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
}
/// Output configuration change result.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum OutputConfigChanged {
/// The target output was connected and the change was applied.
Applied,
/// The target output was not found, the change will be applied when it is connected.
OutputWasMissing,
}
/// A workspace.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Workspace {
/// Index of the workspace on its monitor.
///
/// This is the same index you can use for requests like `niri msg action focus-workspace`.
pub idx: u8,
/// Optional name of the workspace.
pub name: Option<String>,
/// Name of the output that the workspace is on.
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace is currently active on its output.
pub is_active: bool,
}
impl FromStr for WorkspaceReferenceArg {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let reference = if let Ok(index) = s.parse::<i32>() {
if let Ok(idx) = u8::try_from(index) {
Self::Index(idx)
} else {
return Err("workspace indexes must be between 0 and 255");
}
} else {
Self::Name(s.to_string())
};
Ok(reference)
}
}
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""#
)),
}
}
}
impl FromStr for ModeToSet {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("auto") {
return Ok(Self::Automatic);
}
let mode = s.parse()?;
Ok(Self::Specific(mode))
}
}
impl FromStr for ConfiguredMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once('x') else {
return Err("no 'x' separator found");
};
let (height, refresh) = match rest.split_once('@') {
Some((height, refresh)) => (height, Some(refresh)),
None => (rest, None),
};
let width = width.parse().map_err(|_| "error parsing width")?;
let height = height.parse().map_err(|_| "error parsing height")?;
let refresh = refresh
.map(str::parse)
.transpose()
.map_err(|_| "error parsing refresh rate")?;
Ok(Self {
width,
height,
refresh,
})
}
}
impl FromStr for ScaleToSet {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("auto") {
return Ok(Self::Automatic);
}
let scale = s.parse().map_err(|_| "error parsing scale")?;
Ok(Self::Specific(scale))
}
}
+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.7.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.0", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.8", path = ".." }
niri-config = { version = "0.1.8", 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,81 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use 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).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,123 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use super::TestCase;
pub struct GradientArea {
progress: f32,
border: FocusRing,
prev_time: Duration,
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
});
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).to_f64();
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 = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update_render_elements(
g_size,
true,
true,
Rectangle::default(),
CornerRadius::default(),
1.,
);
rv.extend(
self.border
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
[BorderRenderElement::new(
area.size,
g_area,
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
FRAC_PI_4,
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _),
);
rv
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklab {
gradient_format: GradientInterpolation,
}
impl GradientOklab {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientOklab {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklabAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklabAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklab,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientOklabAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklchAlpha {
gradient_format: GradientInterpolation,
}
impl GradientOklchAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Longer,
},
}
}
}
impl TestCase for GradientOklchAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklchDecreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchDecreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Decreasing,
},
}
}
}
impl TestCase for GradientOklchDecreasing {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklchIncreasing {
gradient_format: GradientInterpolation,
}
impl GradientOklchIncreasing {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Increasing,
},
}
}
}
impl TestCase for GradientOklchIncreasing {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklchLonger {
gradient_format: GradientInterpolation,
}
impl GradientOklchLonger {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Longer,
},
}
}
}
impl TestCase for GradientOklchLonger {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientOklchShorter {
gradient_format: GradientInterpolation,
}
impl GradientOklchShorter {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Oklch,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientOklchShorter {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientSrgb {
gradient_format: GradientInterpolation,
}
impl GradientSrgb {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientSrgb {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientSrgbAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientSrgbAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,53 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientSrgbLinear {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinear {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
hue_interpolation: HueInterpolation::Shorter,
},
}
}
}
impl TestCase for GradientSrgbLinear {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,51 @@
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
pub struct GradientSrgbLinearAlpha {
gradient_format: GradientInterpolation,
}
impl GradientSrgbLinearAlpha {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
gradient_format: GradientInterpolation {
color_space: GradientColorSpace::SrgbLinear,
hue_interpolation: Default::default(),
},
}
}
}
impl TestCase for GradientSrgbLinearAlpha {
fn render(
&mut self,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 6, size.h / 3);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
self.gradient_format,
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 0.),
0.,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+234
View File
@@ -0,0 +1,234 @@
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, FloatOrInt};
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: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(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, window.rules()), 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, window.rules()), 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(), None);
}
}
}
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.update_render_elements(&self.output);
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+35
View File
@@ -0,0 +1,35 @@
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 gradient_oklab;
pub mod gradient_oklab_alpha;
pub mod gradient_oklch_alpha;
pub mod gradient_oklch_decreasing;
pub mod gradient_oklch_increasing;
pub mod gradient_oklch_longer;
pub mod gradient_oklch_shorter;
pub mod gradient_srgb;
pub mod gradient_srgb_alpha;
pub mod gradient_srgblinear;
pub mod gradient_srgblinear_alpha;
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>>>;
}
+124
View File
@@ -0,0 +1,124 @@
use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use 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.to_f64(), 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.to_f64(), 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.to_f64(), 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: FloatOrInt(32.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
..Default::default()
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), 1., 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)).to_f64(), 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);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let size = size.to_f64();
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update(
true,
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
);
self.tile
.render(
renderer,
location,
Scale::from(1.),
true,
RenderTarget::Output,
)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+66
View File
@@ -0,0 +1,66 @@
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, size.h - win_size.h))
.to_f64()
.downscale(2.);
self.window
.render(
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+192
View File
@@ -0,0 +1,192 @@
#[macro_use]
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use 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::gradient_oklab::GradientOklab;
use crate::cases::gradient_oklab_alpha::GradientOklabAlpha;
use crate::cases::gradient_oklch_alpha::GradientOklchAlpha;
use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing;
use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing;
use crate::cases::gradient_oklch_longer::GradientOklchLonger;
use crate::cases::gradient_oklch_shorter::GradientOklchShorter;
use crate::cases::gradient_srgb::GradientSrgb;
use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha;
use crate::cases::gradient_srgblinear::GradientSrgbLinear;
use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha;
use crate::cases::layout::Layout;
use crate::cases::tile::Tile;
use crate::cases::window::Window;
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");
s.add(GradientSrgb::new, "Gradient - Srgb");
s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear");
s.add(GradientOklab::new, "Gradient - Oklab");
s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter");
s.add(GradientOklchLonger::new, "Gradient - Oklch Longer");
s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing");
s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing");
s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha");
s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha");
s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha");
s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha");
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();
}
+246
View File
@@ -0,0 +1,246 @@
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::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 mut renderer = GlesRenderer::new(egl_context).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
}
}
+252
View File
@@ -0,0 +1,252 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Serial, 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.to_f64(), [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.to_f64());
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.to_f64());
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<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
SplitElements {
normal: vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
location
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
.into(),
],
popups: vec![],
}
}
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: output::Scale, _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_active_in_column(&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 animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
None
}
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
None
}
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
fn cancel_interactive_resize(&mut self) {}
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
}
+147
View File
@@ -0,0 +1,147 @@
%bcond_without check
%global cargo_install_lib 0
# We want panic backtraces to work without installing the debuginfo package,
# so we leave the debuginfo in the main binary.
%global debug_package %{nil}
%global __strip /bin/true
# To reduce the file size, do some convincing of rust-srpm-macros
# to leave alone the chosen debug settings from Cargo.toml.
%global rustflags_debuginfo please-remove-me
%global build_rustflags %{shrink:
-Copt-level=%rustflags_opt_level
-Ccodegen-units=%rustflags_codegen_units
-Cstrip=none
%{expr:0%{?_include_frame_pointers} && ("%{_arch}" != "ppc64le" && "%{_arch}" != "s390x" && "%{_arch}" != "i386") ? "-Cforce-frame-pointers=yes" : ""}
-Clink-arg=-Wl,-z,relro
-Clink-arg=-Wl,-z,now
%[0%{?_package_note_status} ? "-Clink-arg=%_package_note_flags" : ""]
--cap-lints=warn
}
# Convince rust-srpm-macros to use Cargo.lock with the Smithay commit.
%global __cargo_common_opts %{?_smp_mflags} -Z avoid-dev-deps --locked
%global version {{{ git_dir_version }}}
Name: niri
Version: %{version}
Release: 1%{?dist}
Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 OR BSL-1.0
# Apache-2.0 OR MIT
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
# BSD-2-Clause
# BSD-2-Clause OR Apache-2.0 OR MIT
# BSD-3-Clause
# BSD-3-Clause OR MIT OR Apache-2.0
# GPL-3.0-or-later
# ISC
# MIT
# MIT OR Apache-2.0
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unlicense OR MIT
# Zlib OR Apache-2.0 OR MIT
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
BuildRequires: cargo-rpm-macros >= 26
BuildRequires: pkgconfig(udev)
BuildRequires: pkgconfig(gbm)
BuildRequires: pkgconfig(xkbcommon)
BuildRequires: wayland-devel
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(dbus-1)
BuildRequires: pkgconfig(systemd)
BuildRequires: pkgconfig(libseat)
BuildRequires: pipewire-devel
BuildRequires: pango-devel
BuildRequires: cairo-gobject-devel
# Needed for pipewire-rs
BuildRequires: clang
Requires: mesa-dri-drivers
Requires: mesa-libEGL
# Portal implementations used by niri
Recommends: xdg-desktop-portal-gtk
Recommends: xdg-desktop-portal-gnome
Recommends: gnome-keyring
# Suggested utilities, bound in the default config
Recommends: alacritty
Recommends: fuzzel
Recommends: swaylock
# Suggested utilities
Recommends: swaybg
Recommends: mako
Recommends: swayidle
%description
A scrollable-tiling Wayland compositor.
Windows are arranged in columns on an infinite strip going to the right.
Opening a new window never causes existing windows to resize.
%prep
{{{ git_dir_setup_macro }}}
# Make the version log message look nicer: since we're building not from niri's git repository,
# the git version macro will show its fallback string.
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
%cargo_prep -N
# We're doing an online build.
sed -i 's/^offline = true$//' .cargo/config.toml
# Final step in leaving alone our debug settings.
sed -i 's/.*please-remove-me$//' .cargo/config.toml
%build
%cargo_build
%install
%cargo_install
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
%if %{with check}
%check
%cargo_test -- --workspace --exclude niri-visual-tests
%endif
%files
%license LICENSE
%doc README.md
%doc resources/default-config.kdl
%doc wiki
%{_bindir}/niri
%{_bindir}/niri-session
%{_datadir}/wayland-sessions/niri.desktop
%dir %{_datadir}/xdg-desktop-portal
%{_datadir}/xdg-desktop-portal/niri-portals.conf
%{_userunitdir}/niri.service
%{_userunitdir}/niri-shutdown.target
%changelog
{{{ git_dir_changelog }}}
+205 -109
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,115 +16,98 @@ 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.
// Omitting settings disables them, or leaves them at their default values.
touchpad {
// off
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
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.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// 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"
// You can use integer or fractional scale, for example use 1.5 for 150% scale.
scale 2
// 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 +118,65 @@ 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.
// Changing the color space is also supported, check the wiki for more info.
//
// 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,28 +189,14 @@ layout {
// top 64
// bottom 64
}
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
// - "always", the focused column will always be centered.
center-focused-column "never"
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// spawn-at-startup "alacritty" "-e" "fish"
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 +210,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 +254,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 +265,18 @@ 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:
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
// 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 +332,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 +354,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,10 +416,18 @@ 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+Shift+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
@@ -338,35 +459,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"
}
+2 -8
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
@@ -40,8 +34,8 @@ fi
# Start niri and wait for it to terminate.
systemctl --user --wait start niri.service
# Force stop of grahical-session.target.
# Force stop of graphical-session.target.
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
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
}
}
+379
View File
@@ -0,0 +1,379 @@
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 {
Linear,
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::Linear => x,
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::Linear => Curve::Linear,
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())
}
}
}
+29 -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,8 @@ use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::Niri;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
pub mod tty;
pub use tty::Tty;
@@ -33,6 +32,23 @@ pub enum RenderResult {
Skipped,
}
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutputId(u32);
impl OutputId {
fn next() -> OutputId {
OutputId(OUTPUT_ID_COUNTER.next())
}
pub fn get(self) -> u32 {
self.0
}
}
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
match self {
@@ -98,7 +114,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 +128,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 +146,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(_) => (),
@@ -152,6 +160,13 @@ impl Backend {
}
}
pub fn on_debug_config_changed(&mut self) {
match self {
Backend::Tty(tty) => tty.on_debug_config_changed(),
Backend::Winit(_) => (),
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v
+1146 -306
View File
File diff suppressed because it is too large Load Diff
+82 -52
View File
@@ -15,30 +15,32 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use smithay::reexports::winit::window::Window;
use super::RenderResult;
use crate::niri::{RedrawState, State};
use crate::utils::get_monotonic_time;
use crate::Niri;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
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 {
let builder = WindowBuilder::new()
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = Window::default_attributes()
.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_attributes(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -54,12 +56,12 @@ 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([(
"winit".to_owned(),
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
OutputId::next(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
@@ -69,16 +71,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 +96,61 @@ 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.values_mut().next().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);
let config = self.config.borrow();
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
shaders::set_custom_resize_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
shaders::set_custom_close_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
shaders::set_custom_open_program(renderer, Some(src));
}
drop(config);
niri.layout.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
pub fn seat_name(&self) -> String {
@@ -151,7 +168,18 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
let output_state = niri.output_state.get_mut(output).unwrap();
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Hand them over to winit.
self.backend.bind().unwrap();
@@ -172,10 +200,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 +225,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 +247,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()
}
}
+93
View File
@@ -0,0 +1,93 @@
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::{Action, OutputAction};
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`).
///
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
/// command line argument takes precedence.
#[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 {
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
///
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
/// command line argument takes precedence.
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
pub enum Msg {
/// List connected outputs.
Outputs,
/// List workspaces.
Workspaces,
/// Print information about the focused window.
FocusedWindow,
/// Print information about the focused output.
FocusedOutput,
/// Perform an action.
Action {
#[command(subcommand)]
action: Action,
},
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
/// configuration subsequently changes in the config file, these temporary changes will be
/// forgotten.
Output {
/// Output name.
///
/// Run `niri msg outputs` to see the output names.
#[arg()]
output: String,
/// Configuration to apply.
#[command(subcommand)]
action: OutputAction,
},
/// Print the version of the running niri instance.
Version,
/// Request an error from the running niri instance.
RequestError,
}
-197
View File
@@ -1,197 +0,0 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::time::Duration;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::FontDescription;
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::animation::Animation;
use crate::render_helpers::NiriRenderer;
const TEXT: &str = "Failed to parse the config file. \
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
to see the errors.";
const PADDING: i32 = 8;
const FONT: &str = "sans 14px";
const BORDER: i32 = 4;
pub struct ConfigErrorNotification {
state: State,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
}
enum State {
Hidden,
Showing(Animation),
Shown(Duration),
Hiding(Animation),
}
pub type ConfigErrorNotificationRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ConfigErrorNotification {
pub fn new() -> Self {
Self {
state: State::Hidden,
buffers: RefCell::new(HashMap::new()),
}
}
pub fn show(&mut self) {
// Show from scratch even if already showing to bring attention.
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
}
pub fn hide(&mut self) {
if matches!(self.state, State::Hidden) {
return;
}
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
}
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
match &mut self.state {
State::Hidden => (),
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
}
}
State::Shown(deadline) => {
if target_presentation_time >= *deadline {
self.hide();
}
}
State::Hiding(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Hidden;
}
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
!matches!(self.state, State::Hidden)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<ConfigErrorNotificationRenderElement<R>> {
if matches!(self.state, State::Hidden) {
return None;
}
let scale = output.current_scale().integer_scale();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
Some(0.9),
None,
None,
Kind::Unspecified,
)
.ok()?;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let buffer_size = elem
.geometry(output.current_scale().fractional_scale().into())
.size;
let y_range = buffer_size.h + PADDING * 2 * scale;
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
let y = match &self.state {
State::Hidden => unreachable!(),
State::Showing(anim) | State::Hiding(anim) => {
(-buffer_size.h as f64 + anim.value() * y_range as f64).round() as i32
}
State::Shown(_) => PADDING * 2 * scale,
};
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(1., 0.3, 0.3);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+7 -21
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};
@@ -143,7 +142,7 @@ impl CursorManager {
.unwrap()
}
/// Currenly used cursor_image as a cursor provider.
/// Currently used cursor_image as a cursor provider.
pub fn cursor_image(&self) -> &CursorImageStatus {
&self.current_cursor
}
@@ -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)
}
}
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashMap;
use zbus::fdo::{self, RequestNameFlags};
use zbus::zvariant::{SerializeDict, Type, Value};
use zbus::{dbus_interface, SignalContext};
use super::Start;
pub struct Introspect {
to_niri: calloop::channel::Sender<IntrospectToNiri>,
from_niri: async_channel::Receiver<NiriToIntrospect>,
}
pub enum IntrospectToNiri {
GetWindows,
}
pub enum NiriToIntrospect {
Windows(HashMap<u64, WindowProperties>),
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
pub struct WindowProperties {
/// Window title.
pub title: String,
/// Window app ID.
///
/// This is actually the name of the .desktop file, and Shell does internal tracking to match
/// Wayland app IDs to desktop files. We don't do that yet, which is the reason why
/// xdg-desktop-portal-gnome's window list is missing icons.
#[zvariant(rename = "app-id")]
pub app_id: String,
}
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
impl Introspect {
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
warn!("error sending message to niri: {err:?}");
return Err(fdo::Error::Failed("internal error".to_owned()));
}
match self.from_niri.recv().await {
Ok(NiriToIntrospect::Windows(windows)) => Ok(windows),
Err(err) => {
warn!("error receiving message from niri: {err:?}");
Err(fdo::Error::Failed("internal error".to_owned()))
}
}
}
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
// needed for the event stream IPC anyway).
#[dbus_interface(signal)]
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl Introspect {
pub fn new(
to_niri: calloop::channel::Sender<IntrospectToNiri>,
from_niri: async_channel::Receiver<NiriToIntrospect>,
) -> Self {
Self { to_niri, from_niri }
}
}
impl Start for Introspect {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/gnome/Shell/Introspect", self)?;
conn.request_name_with_flags("org.gnome.Shell.Introspect", flags)?;
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};
+28 -8
View File
@@ -1,9 +1,10 @@
use smithay::reexports::calloop;
use zbus::blocking::Connection;
use zbus::Interface;
use crate::niri::State;
pub mod freedesktop_screensaver;
pub mod gnome_shell_introspect;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
@@ -13,6 +14,8 @@ pub mod mutter_screen_cast;
#[cfg(feature = "xdp-gnome-screencast")]
use mutter_screen_cast::ScreenCast;
use self::freedesktop_screensaver::ScreenSaver;
use self::gnome_shell_introspect::Introspect;
use self::mutter_display_config::DisplayConfig;
use self::mutter_service_channel::ServiceChannel;
@@ -24,7 +27,9 @@ 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>,
pub conn_introspect: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
}
@@ -45,9 +50,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
@@ -61,22 +69,34 @@ impl DBusServers {
let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri);
dbus.conn_screen_shot = try_start(screenshot);
let (to_niri, from_introspect) = calloop::channel::channel();
let (to_introspect, from_niri) = async_channel::unbounded();
niri.event_loop
.insert_source(from_introspect, move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => {
state.on_introspect_msg(&to_introspect, msg)
}
calloop::channel::Event::Closed => (),
})
.unwrap();
let introspect = Introspect::new(to_niri, from_niri);
dbus.conn_introspect = try_start(introspect);
#[cfg(feature = "xdp-gnome-screencast")]
{
if niri.pipewire.is_some() {
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
let to_niri = to_niri.clone();
move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => {
state.on_screen_cast_msg(&to_niri, msg)
}
calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg),
calloop::channel::Event::Closed => (),
}
})
.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");
}
}
+78 -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,13 +53,16 @@ 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| {
.values()
// Take only enabled outputs.
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
.map(|output| {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
// FIXME: use proper serial when we have libdisplay-info.
@@ -78,46 +81,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 }
}
}
+135 -25
View File
@@ -4,17 +4,16 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
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 +22,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)]
@@ -46,25 +46,59 @@ struct RecordMonitorProperties {
_is_recording: Option<bool>,
}
#[derive(Debug, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
struct RecordWindowProperties {
#[zvariant(rename = "window-id")]
window_id: u64,
#[zvariant(rename = "cursor-mode")]
cursor_mode: Option<CursorMode>,
#[zvariant(rename = "is-recording")]
_is_recording: Option<bool>,
}
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct Stream {
output: Output,
target: StreamTarget,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Clone)]
enum StreamTarget {
// FIXME: update on scale changes and whatnot.
Output(niri_ipc::Output),
Window { id: u64 },
}
#[derive(Debug, Clone)]
pub enum StreamTargetId {
Output { name: String },
Window { id: u64 },
}
#[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,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -85,11 +119,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 +159,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,20 +191,63 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
let output = {
let ipc_outputs = self.ipc_outputs.lock().unwrap();
ipc_outputs.values().find(|o| o.name == connector).cloned()
};
let Some(output) = output else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
static NUMBER: AtomicUsize = AtomicUsize::new(0);
if output.logical.is_none() {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
NUMBER.fetch_add(1, Ordering::SeqCst)
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
let target = StreamTarget::Output(output);
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
self.streams.lock().unwrap().push((stream, iface));
}
Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())),
Err(err) => {
return Err(fdo::Error::Failed(format!(
"error creating stream object: {err:?}"
)))
}
}
Ok(path)
}
async fn record_window(
&mut self,
#[zbus(object_server)] server: &ObjectServer,
properties: RecordWindowProperties,
) -> fdo::Result<OwnedObjectPath> {
debug!(?properties, "record_window");
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
STREAM_ID.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let target = StreamTarget::Window {
id: properties.window_id,
};
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -195,15 +273,35 @@ 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 {
match &self.target {
StreamTarget::Output(output) => {
let logical = output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
}
}
StreamTarget::Window { .. } => {
// Does any consumer need this?
StreamParameters {
position: (0, 0),
size: (1, 1),
}
}
}
}
}
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 +326,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)),
}
}
}
@@ -249,13 +348,13 @@ impl Drop for Session {
}
impl Stream {
pub fn new(
output: Output,
fn new(
target: StreamTarget,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
output,
target,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
to_niri,
@@ -269,7 +368,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast {
session_id,
output: self.output.clone(),
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
@@ -279,3 +378,14 @@ impl Stream {
}
}
}
impl StreamTarget {
fn make_id(&self) -> StreamTargetId {
match self {
StreamTarget::Output(output) => StreamTargetId::Output {
name: output.name.clone(),
},
StreamTarget::Window { id } => StreamTargetId::Window { id: *id },
}
}
}
+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)
}
}
}
+207 -43
View File
@@ -8,17 +8,18 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Client, Resource};
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, send_surface_state,
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
SurfaceAttributes,
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, with_states,
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
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::utils::send_scale_transform;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -36,10 +37,10 @@ impl CompositorHandler for State {
}
if let Some(output) = self.niri.output_for_root(&root) {
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
send_scale_transform(surface, data, scale, transform);
});
}
}
@@ -49,33 +50,36 @@ impl CompositorHandler for State {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
.cached_state
.pending::<SurfaceAttributes>()
.get::<SurfaceAttributes>()
.pending()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok(),
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
_ => None,
})
});
if let Some(dmabuf) = maybe_dmabuf {
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
let client = surface.client().unwrap();
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
let display_handle = state.niri.display_handle.clone();
if let Some(client) = surface.client() {
let res =
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
.niri
.event_loop
.insert_source(source, move |_, _, state| {
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
}
}
}
}
})
});
}
fn commit(&mut self, surface: &WlSurface) {
@@ -93,55 +97,189 @@ 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, workspace_name) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name,
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
// Check that the workspace still exists.
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
(rules, width, is_full_width, output, workspace_name)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None, 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(workspace_name) = &workspace_name {
self.niri.layout.add_window_to_named_workspace(
workspace_name,
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);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
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);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: u64::from(id.get()),
});
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;
}
let serial = with_states(surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
role.configure_serial
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
}
// The toplevel remains mapped.
self.niri.layout.update_window(&window);
self.niri.layout.update_window(&window, serial);
// 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 +288,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.layout.update_window(&window, None);
self.niri.queue_redraw(&output);
return;
}
@@ -161,7 +301,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());
}
}
@@ -185,14 +325,38 @@ impl CompositorHandler for State {
if self.niri.is_locked() {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == surface {
self.niri.queue_redraw(output.clone());
if lock_surface.wl_surface() == &root_surface {
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 <= 0.13 (it was fixed in winit afterwards:
// https://github.com/rust-windowing/winit/pull/3625).
//
// This is still not perfect, as this function is called already after the (first)
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
}
}
self.niri
.root_surface
.retain(|k, v| k != surface && v != surface);
}
}
impl BufferHandler for State {
+19 -12
View File
@@ -1,9 +1,9 @@
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, WindowSurfaceType};
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::wlr_layer::{
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
@@ -11,6 +11,7 @@ use smithay::wayland::shell::wlr_layer::{
use smithay::wayland::shell::xdg::PopupSurface;
use crate::niri::State;
use crate::utils::send_scale_transform;
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -24,11 +25,17 @@ impl WlrLayerShellHandler for State {
_layer: Layer,
namespace: String,
) {
let output = wl_output
.as_ref()
.and_then(Output::from_resource)
.or_else(|| self.niri.layout.active_output().cloned())
.unwrap();
let output = if let Some(wl_output) = &wl_output {
Output::from_resource(wl_output)
} else {
self.niri.layout.active_output().cloned()
};
let Some(output) = output else {
warn!("no output for new layer surface, closing");
surface.send_close();
return;
};
let mut map = layer_map_for_output(&output);
map.map_layer(&LayerSurface::new(surface, namespace))
.unwrap();
@@ -50,12 +57,12 @@ impl WlrLayerShellHandler for State {
None
};
if let Some(output) = output {
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
self.unconstrain_popup(&popup);
self.unconstrain_popup(&PopupKind::Xdg(popup));
}
}
delegate_layer_shell!(State);
@@ -97,16 +104,16 @@ impl State {
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
.unwrap();
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
send_scale_transform(surface, data, scale, transform);
});
layer.layer_surface().send_configure();
}
drop(map);
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
+324 -21
View File
@@ -9,18 +9,30 @@ 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::rustix::fs::{fcntl_setfl, OFlags};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::compositor::with_states;
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
@@ -37,20 +49,38 @@ use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
use smithay::wayland::session_lock::{
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
};
use smithay::wayland::tablet_manager::TabletSeatHandler;
use smithay::wayland::xdg_activation::{
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_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_tablet_manager, delegate_text_input_manager, delegate_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, State};
use crate::utils::output_size;
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::utils::{output_size, send_scale_transform};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management,
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 +103,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(
@@ -93,29 +145,39 @@ delegate_pointer_constraints!(State);
impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
let popup = PopupKind::from(surface.clone());
let popup = PopupKind::InputMethod(surface);
if let Some(output) = self.output_for_popup(&popup) {
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
let wl_surface = popup.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
}
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking ime popup {err:?}");
}
}
fn popup_repositioned(&mut self, surface: PopupSurface) {
let popup = PopupKind::InputMethod(surface);
self.unconstrain_popup(&popup);
}
fn dismiss_popup(&mut self, surface: PopupSurface) {
if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) {
let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
}
}
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
self.niri
.layout
.find_window_and_output(parent)
.map(|(window, _)| window.geometry())
.map(|(mapped, _)| mapped.window.geometry())
.unwrap_or_default()
}
}
@@ -138,6 +200,10 @@ impl SelectionHandler for State {
let buf = user_data.clone();
thread::spawn(move || {
// Clear O_NONBLOCK, otherwise File::write_all() will stop halfway.
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
warn!("error clearing flags on selection target fd: {err:?}");
}
if let Err(err) = File::from(fd).write_all(&buf) {
warn!("error writing selection: {err:?}");
}
@@ -189,6 +255,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 +275,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();
}
}
}
@@ -231,7 +299,7 @@ impl SessionLockHandler for State {
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
let Some(output) = Output::from_resource(&output) else {
error!("no Output matching WlOutput");
warn!("no Output matching WlOutput");
return;
};
@@ -246,11 +314,11 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
let size = output_size(output);
states.size = Some(Size::from((size.w as u32, size.h as u32)));
});
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
surface.send_configure();
}
@@ -268,7 +336,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 +345,238 @@ 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.layer_shell_on_demand_focus = None;
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, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
// If with_damage then push it onto the queue for redraw of the output,
// otherwise render it immediately.
if screencopy.with_damage() {
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
trace!("screencopy manager destroyed already");
return;
};
queue.push(screencopy);
} else {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self
.niri
.render_for_screencopy_without_damage(renderer, manager, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
}
});
}
}
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState {
&mut self.niri.screencopy_state
}
}
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);
impl XdgActivationHandler for State {
fn activation_state(&mut self) -> &mut XdgActivationState {
&mut self.niri.activation_state
}
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
// Only tokens that were created while the application has keyboard focus are valid.
let Some((serial, seat)) = data.serial else {
return false;
};
let Some(seat) = Seat::<State>::from_resource(&seat) else {
return false;
};
let keyboard = seat.get_keyboard().unwrap();
keyboard
.last_enter()
.map(|last_enter| serial.is_no_older_than(&last_enter))
.unwrap_or(false)
}
fn request_activation(
&mut self,
_token: XdgActivationToken,
token_data: XdgActivationTokenData,
surface: WlSurface,
) {
if token_data.timestamp.elapsed().as_secs() < 10 {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
}
}
}
}
delegate_xdg_activation!(State);
impl FractionalScaleHandler for State {}
delegate_fractional_scale!(State);
impl OutputManagementHandler for State {
fn output_management_state(&mut self) -> &mut OutputManagementManagerState {
&mut self.niri.output_management_state
}
fn apply_output_config(&mut self, config: niri_config::Outputs) {
self.niri.config.borrow_mut().outputs = config;
self.reload_output_config();
}
}
delegate_output_management!(State);
+658 -120
View File
@@ -1,5 +1,7 @@
use std::cell::Cell;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
@@ -7,23 +9,35 @@ use smithay::input::pointer::Focus;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self};
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
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::reexports::wayland_server::{self, Resource, WEnum};
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::compositor::{
add_pre_commit_hook, 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::wlr_layer::{self, Layer};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
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::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
use crate::utils::{get_monotonic_time, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -32,34 +46,16 @@ 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());
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
self.unconstrain_popup(&surface);
let popup = PopupKind::Xdg(surface);
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking popup: {err:?}");
}
}
@@ -70,12 +66,73 @@ impl XdgShellHandler for State {
fn resize_request(
&mut self,
_surface: ToplevelSurface,
surface: ToplevelSurface,
_seat: WlSeat,
_serial: Serial,
_edges: ResizeEdge,
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
// FIXME
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
}
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let edges = ResizeEdge::from(edges);
let window = mapped.window.clone();
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
last_cell.set(Some((time, edges)));
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
last_cell.set(None);
let intersection = edges.intersection(last_edges);
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.reset_window_height();
}
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
fn reposition_request(
@@ -89,11 +146,20 @@ impl XdgShellHandler for State {
state.geometry = geometry;
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
self.unconstrain_popup(&PopupKind::Xdg(surface.clone()));
surface.send_repositioned(token);
}
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;
@@ -120,20 +186,25 @@ impl XdgShellHandler for State {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
&& layers.layers_on(Layer::Top).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
@@ -177,7 +248,7 @@ impl XdgShellHandler for State {
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
@@ -185,9 +256,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 +269,181 @@ 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 { rules, 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, rules);
}
// 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 {
rules,
width,
is_full_width,
output,
workspace_name,
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = workspace_name
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name))
.map(|mon| (mon, false));
let mon = mon.or_else(|| {
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 = workspace_name
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
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, rules);
}
// 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 +463,55 @@ 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();
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: u64::from(mapped.id().get()),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
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);
@@ -318,19 +559,53 @@ impl XdgDecorationHandler for State {
}
delegate_xdg_decoration!(State);
/// Whether KDE server decorations are in use.
#[derive(Default)]
pub struct KdeDecorationsModeState {
server: Cell<bool>,
}
impl KdeDecorationsModeState {
pub fn is_server(&self) -> bool {
self.server.get()
}
}
impl KdeDecorationHandler for State {
fn kde_decoration_state(&self) -> &KdeDecorationState {
&self.niri.kde_decoration_state
}
}
delegate_kde_decoration!(State);
fn request_mode(
&mut self,
surface: &WlSurface,
decoration: &org_kde_kwin_server_decoration::OrgKdeKwinServerDecoration,
mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
) {
let WEnum::Value(mode) = mode else {
return;
};
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
if !initial_configure_sent(toplevel) {
toplevel.send_configure();
decoration.mode(mode);
with_states(surface, |states| {
let state = states
.data_map
.get_or_insert(KdeDecorationsModeState::default);
state
.server
.set(mode == org_kde_kwin_server_decoration::Mode::Server);
});
}
}
delegate_kde_decoration!(State);
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 +620,149 @@ 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),
self.niri.is_at_startup,
);
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 a workspace set in the window rules.
let mon = rules
.open_on_workspace
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name));
// If not, check if we had an output set in the window rules.
let mon = mon.or_else(|| {
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 = rules
.open_on_workspace
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
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, &rules);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
};
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);
@@ -364,17 +782,20 @@ impl State {
if !initial_configure_sent {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
{
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
send_scale_transform(surface, data, scale, transform);
});
}
popup.send_configure().expect("initial configure failed");
}
}
// Input method popups don't require a configure.
PopupKind::InputMethod(_) => (),
// Input method popup can arbitrary change its geometry, so we need to unconstrain
// it on commit.
PopupKind::InputMethod(_) => {
self.unconstrain_popup(&popup);
}
}
}
}
@@ -384,18 +805,18 @@ impl State {
self.niri.output_for_root(&root)
}
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
pub fn unconstrain_popup(&self, popup: &PopupKind) {
let _span = tracy_client::span!("Niri::unconstrain_popup");
// Popups with a NULL parent will get repositioned in their respective protocol handlers
// (i.e. layer-shell).
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
let Ok(root) = find_popup_root_surface(popup) else {
return;
};
// 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)?;
@@ -405,7 +826,7 @@ impl State {
}
}
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
@@ -416,18 +837,16 @@ impl State {
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
target.loc.y -= self.niri.layout.window_y(window).unwrap();
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
target.loc -= get_popup_toplevel_coords(popup).to_f64();
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
self.position_popup_within_rect(popup, target);
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupSurface,
popup: &PopupKind,
layer_surface: &LayerSurface,
output: &Output,
) {
@@ -441,21 +860,59 @@ impl State {
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
target.loc -= get_popup_toplevel_coords(popup);
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
self.position_popup_within_rect(popup, target.to_f64());
}
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
match popup {
PopupKind::Xdg(popup) => {
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
PopupKind::InputMethod(popup) => {
let text_input_rectangle = popup.text_input_rectangle();
let mut bbox =
utils::bbox_from_surface_tree(popup.wl_surface(), text_input_rectangle.loc)
.to_f64();
// Position bbox horizontally first.
let overflow_x = (bbox.loc.x + bbox.size.w) - (target.loc.x + target.size.w);
if overflow_x > 0. {
bbox.loc.x -= overflow_x;
}
// Ensure that the popup starts within the window.
bbox.loc.x = f64::max(bbox.loc.x, target.loc.x);
// Try to position IME popup below the text input rectangle.
let mut below = bbox;
below.loc.y += f64::from(text_input_rectangle.size.h);
let mut above = bbox;
above.loc.y -= bbox.size.h;
if target.loc.y + target.size.h >= below.loc.y + below.size.h {
popup.set_location(below.loc.to_i32_round());
} else {
popup.set_location(above.loc.to_i32_round());
}
}
}
}
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()) {
match popup {
PopupKind::Xdg(ref popup) => {
for (popup, _) in PopupManager::popups_for_surface(
window.toplevel().expect("no x11 support").wl_surface(),
) {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(popup, window, output);
self.unconstrain_window_popup(xdg_popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
@@ -465,29 +922,60 @@ 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),
self.niri.is_at_startup,
);
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, self.niri.is_at_startup) {
drop(config);
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window, None);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
fn unconstrain_with_padding(
positioner: PositionerState,
target: Rectangle<i32, Logical>,
target: Rectangle<f64, Logical>,
) -> Rectangle<i32, Logical> {
// Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try
// unconstraining without padding.
const PADDING: i32 = 8;
const PADDING: f64 = 8.;
let mut padded = target;
if PADDING * 2 < padded.size.w {
if PADDING * 2. < padded.size.w {
padded.loc.x += PADDING;
padded.size.w -= PADDING * 2;
padded.size.w -= PADDING * 2.;
}
if PADDING * 2 < padded.size.h {
if PADDING * 2. < padded.size.h {
padded.loc.y += PADDING;
padded.size.h -= PADDING * 2;
padded.size.h -= PADDING * 2.;
}
// No padding, so just unconstrain with the original target.
if padded == target {
return positioner.get_unconstrained_geometry(target);
return positioner.get_unconstrained_geometry(target.to_i32_round());
}
// Do not try to resize to fit the padded target rectangle.
@@ -499,11 +987,61 @@ fn unconstrain_with_padding(
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeY);
let geo = no_resize.get_unconstrained_geometry(padded);
if padded.contains_rect(geo) {
let geo = no_resize.get_unconstrained_geometry(padded.to_i32_round());
if padded.contains_rect(geo.to_f64()) {
return geo;
}
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target)
positioner.get_unconstrained_geometry(target.to_i32_round())
}
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 got_unmapped = {
let mut guard = states.cached_state.get::<SurfaceAttributes>();
matches!(guard.pending().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
};
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
mapped.store_animation_snapshot(renderer);
});
}
// The toplevel remains mapped; clear any stored unmap snapshot.
state.niri.layout.clear_unmap_snapshot(&window);
}
})
}
-1645
View File
File diff suppressed because it is too large Load Diff
+2903
View File
File diff suppressed because it is too large Load Diff
+176
View File
@@ -0,0 +1,176 @@
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct ResizeGrab {
start_data: PointerGrabStartData<State>,
window: Window,
}
impl ResizeGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self { start_data, window }
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for ResizeGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
let delta = event.location - self.start_data.location;
let ongoing = data
.niri
.layout
.interactive_resize_update(&self.window, delta);
if ongoing {
return;
}
}
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+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.;
}
}
+231
View File
@@ -0,0 +1,231 @@
use std::time::Duration;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => None,
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(false, Some(false))
}
};
if let Some(output) = res {
state.niri.queue_redraw(&output);
}
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for SpatialMovementGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
let timestamp = Duration::from_millis(u64::from(event.time));
let delta = event.location - self.last_location;
self.last_location = event.location;
let layout = &mut data.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
layout.workspace_switch_gesture_update(-c.y, timestamp, false)
}
} else {
Some(None)
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_update(-delta.x, timestamp, false)
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_update(-delta.y, timestamp, false)
}
};
if let Some(output) = res {
if let Some(output) = output {
data.niri.queue_redraw(&output);
}
} else {
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+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();
}
}
}
+310 -78
View File
@@ -1,46 +1,109 @@
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, OutputConfigChanged, 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::FocusedOutput => Request::FocusedOutput,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
action: action.clone(),
},
Msg::Workspaces => Request::Workspaces,
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:?}");
};
@@ -55,52 +118,221 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
for (connector, output) in outputs.into_iter() {
let Output {
name,
make,
model,
physical_size,
modes,
current_mode,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
} else {
println!(" Disabled");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
println!(" Physical size: unknown");
}
println!(" Available modes:");
for mode in modes {
let Mode {
width,
height,
refresh_rate,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" {width}x{height}@{refresh:.3}");
}
print_output(connector, output)?;
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::FocusedOutput => {
let Response::FocusedOutput(output) = response else {
bail!("unexpected response: expected FocusedOutput, got {response:?}");
};
if json {
let output = serde_json::to_string(&output).context("error formatting response")?;
println!("{output}");
return Ok(());
}
if let Some(output) = output {
print_output(output.name.clone(), output)?;
} else {
println!("No output is focused.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
Msg::Output { output, .. } => {
let Response::OutputConfigChanged(response) = response else {
bail!("unexpected response: expected OutputConfigChanged, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response == OutputConfigChanged::OutputWasMissing {
println!("Output \"{output}\" is not connected.");
println!("The change will apply when it is connected.");
}
}
Msg::Workspaces => {
let Response::Workspaces(mut response) = response else {
bail!("unexpected response: expected Workspaces, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response.is_empty() {
println!("No workspaces.");
return Ok(());
}
response.sort_by_key(|ws| ws.idx);
response.sort_by(|a, b| a.output.cmp(&b.output));
let mut current_output = if let Some(output) = response[0].output.as_deref() {
println!("Output \"{output}\":");
Some(output)
} else {
println!("No output:");
None
};
for ws in &response {
if ws.output.as_deref() != current_output {
let output = ws.output.as_deref().context(
"invalid response: workspace with no output \
following a workspace with an output",
)?;
current_output = Some(output);
println!("\nOutput \"{output}\":");
}
let is_active = if ws.is_active { " * " } else { " " };
let idx = ws.idx;
let name = if let Some(name) = ws.name.as_deref() {
format!(" \"{name}\"")
} else {
String::new()
};
println!("{is_active}{idx}{name}");
}
}
}
Ok(())
}
fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
let Output {
name,
make,
model,
physical_size,
modes,
current_mode,
vrr_supported,
vrr_enabled,
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
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 (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
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}");
}
Ok(())
}
+130 -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::{OutputConfigChanged, 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,127 @@ 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 = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
let buf = serde_json::to_vec(&response).context("error formatting response")?;
write
.write_all(&buf)
.await
.context("error writing response")?;
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
write.write_all(&buf).await.context("error writing reply")?;
Ok(())
}
async 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();
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
Response::Outputs(outputs.collect())
}
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 (tx, rx) = async_channel::bounded(1);
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action, false);
let _ = tx.send_blocking(());
});
// Wait until the action has been processed before returning. This is important for a
// few actions, for instance for DoScreenTransition this wait ensures that the screen
// contents were sampled into the texture.
let _ = rx.recv().await;
Response::Handled
}
Request::Output { output, action } => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.values()
.any(|o| o.name.eq_ignore_ascii_case(&output));
let response = if found {
OutputConfigChanged::Applied
} else {
OutputConfigChanged::OutputWasMissing
};
drop(ipc_outputs);
ctx.event_loop.insert_idle(move |state| {
state.apply_transient_output_config(&output, action);
});
Response::OutputConfigChanged(response)
}
Request::Workspaces => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let workspaces = state.niri.layout.ipc_workspaces();
let _ = tx.send_blocking(workspaces);
});
let result = rx.recv().await;
let workspaces = result.map_err(|_| String::from("error getting workspace info"))?;
Response::Workspaces(workspaces)
}
Request::FocusedOutput => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let active_output = state
.niri
.layout
.active_output()
.map(|output| output.name());
let output = active_output.and_then(|active_output| {
state
.backend
.ipc_outputs()
.lock()
.unwrap()
.values()
.find(|o| o.name == active_output)
.cloned()
});
let _ = tx.send_blocking(output);
});
let result = rx.recv().await;
let output = result.map_err(|_| String::from("error getting active output info"))?;
Response::FocusedOutput(output)
}
};
Ok(response)
}
+212
View File
@@ -0,0 +1,212 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
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>,
/// Size of the window geometry.
geo_size: Size<f64, Logical>,
/// Position in the workspace.
pos: Point<f64, Logical>,
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
/// The closing animation.
anim: Animation,
/// Random seed for the shader.
random_seed: f32,
}
niri_render_elements! {
ClosingWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
impl ClosingWindow {
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: Scale<f64>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
anim: Animation,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
let mut render_to_texture = |elements: Vec<E>| -> anyhow::Result<_> {
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
)
.context("error rendering to texture")?;
let buffer = TextureBuffer::from_texture(
renderer,
texture,
scale,
Transform::Normal,
Vec::new(),
);
let offset = geo.loc.to_f64().to_logical(scale);
Ok((buffer, offset))
};
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
blocked_out_buffer_offset,
anim,
random_seed: fastrand::f32(),
})
}
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_done()
}
pub fn render(
&self,
renderer: &mut GlesRenderer,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.buffer, self.buffer_offset)
};
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
// Round to physical pixels relative to the view position. This is similar to what
// happens when rendering normal windows.
let relative = self.pos - view_rect.loc;
let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale);
let geo_loc = Vec2::new(pos.x as f32, pos.y as f32);
let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = self.buffer.texture_scale();
let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = self.buffer.texture().size();
let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return ShaderRenderElement::new(
ProgramType::Close,
view_rect.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
Kind::Unspecified,
)
.with_location(Point::from((0., 0.)))
.into();
}
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
1. - clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = self.geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
((1. - clamped_progress) / 5. + 0.8).max(0.),
);
let mut location = self.pos + offset;
location.x -= view_rect.loc.x;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
);
elem.into()
}
}
+218 -65
View File
@@ -1,115 +1,268 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{self, Color};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
is_off: bool,
buffers: [SolidColorBuffer; 8],
locations: [Point<f64, Logical>; 8],
sizes: [Size<f64, Logical>; 8],
borders: [BorderRenderElement; 8],
full_size: Size<f64, Logical>,
is_border: bool,
width: i32,
active_color: Color,
inactive_color: Color,
use_border_shader: bool,
config: niri_config::FocusRing,
}
pub type FocusRingRenderElement = SolidColorRenderElement;
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = BorderRenderElement,
}
}
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
is_off: config.off,
sizes: Default::default(),
borders: Default::default(),
full_size: Default::default(),
is_border: false,
width: config.width.into(),
active_color: config.active_color,
inactive_color: config.inactive_color,
use_border_shader: false,
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_shaders(&mut self) {
for elem in &mut self.borders {
elem.damage_all();
}
}
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));
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.active_color
} 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.config.inactive_color
};
for buf in &mut self.buffers {
buf.set_color(color.to_array_premul());
}
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
let gradient = if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
let rounded_corner_border_width = if self.is_border {
// HACK: increase the border width used for the inner rounded corners a tiny bit to
// reduce background bleed.
width as f32 + 0.5
} else {
0.
};
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size and border width are rounded to physical pixels before being passed to this
// function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
if is_border {
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
let top_right = f64::min(
self.full_size.w - top_left,
f64::max(width, ceil(f64::from(radius.top_right))),
);
let bottom_left = f64::min(
self.full_size.h - top_left,
f64::max(width, ceil(f64::from(radius.bottom_left))),
);
let bottom_right = f64::min(
self.full_size.h - top_right,
f64::min(
self.full_size.w - bottom_left,
f64::max(width, ceil(f64::from(radius.bottom_right))),
),
);
// Top edge.
self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width));
self.locations[0] = Point::from((-width + top_left, -width));
// Bottom edge.
self.sizes[1] =
Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width));
self.locations[1] = Point::from((-width + bottom_left, win_size.h));
// Left edge.
self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left));
self.locations[2] = Point::from((-width, -width + top_left));
// Right edge.
self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right));
self.locations[3] = Point::from((win_size.w, -width + top_right));
// Top-left corner.
self.sizes[4] = Size::from((top_left, top_left));
self.locations[4] = Point::from((-width, -width));
// Top-right corner.
self.sizes[5] = Size::from((top_right, top_right));
self.locations[5] = Point::from((win_size.w + width - top_right, -width));
// Bottom-right corner.
self.sizes[6] = Size::from((bottom_right, bottom_right));
self.locations[6] = Point::from((
win_size.w + width - bottom_right,
win_size.h + width - bottom_right,
));
// Bottom-left corner.
self.sizes[7] = Size::from((bottom_left, bottom_left));
self.locations[7] = Point::from((-width, win_size.h + width - bottom_left));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
self.borders[0].update(
self.sizes[0],
Rectangle::from_loc_and_size(
gradient_area.loc - self.locations[0],
gradient_area.size,
),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
self.is_border = is_border;
}
pub fn set_active(&mut self, is_active: bool) {
let color = if is_active {
self.active_color.into()
} else {
self.inactive_color.into()
};
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 8>::new();
for buf in &mut self.buffers {
buf.set_color(color);
}
}
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.is_off {
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 border_width = -self.locations[0].y;
// If drawing as a border with width = 0, then there's nothing to draw.
if self.is_border && border_width == 0. {
return rv.into_iter();
}
let has_border_shader = BorderRenderElement::has_shader(renderer);
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
};
rv.push(elem);
};
if self.is_border {
for (buf, loc) in zip(&self.buffers, self.locations) {
push(buf, loc);
for ((buf, border), loc) in zip(zip(&self.buffers, &self.borders), self.locations) {
push(buf, border, location + loc);
}
} else {
push(&self.buffers[0], self.locations[0]);
push(
&self.buffers[0],
&self.borders[0],
location + self.locations[0],
);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.width
pub fn width(&self) -> f64 {
self.config.width.0
}
pub fn is_off(&self) -> bool {
self.is_off
self.config.off
}
}
+2186 -290
View File
File diff suppressed because it is too large Load Diff
+492 -66
View File
@@ -2,21 +2,32 @@ 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 smithay::utils::{Logical, Point, Rectangle};
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::utils::output_size;
use crate::input::swipe_tracker::SwipeTracker;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::{output_size, to_physical_precise_round, ResizeEdge};
/// 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> {
@@ -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.
@@ -41,9 +54,12 @@ pub enum WorkspaceSwitch {
#[derive(Debug)]
pub struct WorkspaceSwitchGesture {
/// Index of the workspace where the gesture was started.
pub center_idx: usize,
center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
}
pub type MonitorRenderElement<R> =
@@ -57,6 +73,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 +95,32 @@ 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 find_named_workspace(&self, workspace_name: &str) -> Option<&Workspace<W>> {
self.workspaces.iter().find(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
pub fn find_named_workspace_index(&self, workspace_name: &str) -> Option<usize> {
self.workspaces.iter().position(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
@@ -86,18 +130,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 +175,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];
@@ -154,7 +222,7 @@ impl<W: LayoutElement> Monitor<W> {
continue;
}
if !self.workspaces[idx].has_windows() {
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
@@ -163,6 +231,20 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn unname_workspace(&mut self, workspace_name: &str) -> bool {
for ws in &mut self.workspaces {
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
{
ws.unname();
return true;
}
}
false
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}
@@ -216,6 +298,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();
}
@@ -232,6 +322,14 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().focus_column_last();
}
pub fn focus_column_right_or_first(&mut self) {
self.active_workspace().focus_column_right_or_first();
}
pub fn focus_column_left_or_last(&mut self) {
self.active_workspace().focus_column_left_or_last();
}
pub fn focus_down(&mut self) {
self.active_workspace().focus_down();
}
@@ -240,6 +338,62 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().focus_up();
}
pub fn focus_down_or_left(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_down();
}
}
}
pub fn focus_down_or_right(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.focus_right();
} else {
workspace.focus_down();
}
}
}
pub fn focus_up_or_left(&mut self) {
let workspace = self.active_workspace();
if !workspace.columns.is_empty() {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_up();
}
}
}
pub fn focus_up_or_right(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.focus_left();
} else {
workspace.focus_up();
}
}
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
@@ -287,8 +441,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 +464,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 +487,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,12 +566,36 @@ impl<W: LayoutElement> Monitor<W> {
));
}
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
// Don't animate this action.
self.workspace_switch = None;
fn previous_workspace_idx(&self) -> Option<usize> {
let id = self.previous_workspace_id?;
self.workspaces.iter().position(|w| w.id() == id)
}
self.clean_up_workspaces();
pub fn switch_workspace(&mut self, idx: usize, animate: bool) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
if !animate {
self.workspace_switch = None;
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, false);
}
} else {
self.switch_workspace(idx, false);
}
}
pub fn switch_workspace_previous(&mut self) {
if let Some(idx) = self.previous_workspace_idx() {
self.switch_workspace(idx, false);
}
}
pub fn consume_into_column(&mut self) {
@@ -439,7 +620,7 @@ impl<W: LayoutElement> Monitor<W> {
Some(column.tiles[column.active_tile_idx].window())
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
@@ -449,7 +630,7 @@ impl<W: LayoutElement> Monitor<W> {
}
for ws in &mut self.workspaces {
ws.advance_animations(current_time, is_active);
ws.advance_animations(current_time);
}
}
@@ -462,7 +643,39 @@ 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_render_elements(&mut self, is_active: bool) {
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return;
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
self.workspaces[after_idx].update_render_elements(is_active);
if before_idx < 0. {
return;
}
}
let before_idx = before_idx as usize;
self.workspaces[before_idx].update_render_elements(is_active);
}
None => {
self.workspaces[self.active_workspace_idx].update_render_elements(is_active);
}
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
@@ -471,11 +684,13 @@ impl<W: LayoutElement> Monitor<W> {
}
if self.options.struts != options.struts {
let scale = self.output.current_scale();
let transform = self.output.current_transform();
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(view_size, working_area);
ws.set_view_size(scale, transform, view_size, working_area);
}
}
@@ -498,6 +713,10 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().set_window_height(change);
}
pub fn reset_window_height(&mut self) {
self.active_workspace().reset_window_height();
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
@@ -512,8 +731,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,34 +753,69 @@ 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<f64, 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).to_f64();
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = offset * size.h;
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>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
) -> Option<(&W, Option<Point<f64, Logical>>)> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output);
let size = output_size(&self.output).to_f64();
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;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
(before_idx, Point::from((0, offset)))
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 {
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0., offset)))
} else {
(after_idx, Point::from((0, -size.h + offset)))
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0., -size.h + offset)))
};
let ws = &self.workspaces[idx];
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset)?;
Some((win, win_pos.map(|p| p - ws_offset)))
}
None => {
@@ -569,6 +825,47 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = (render_idx - before_idx) * size.h;
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 {
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)))
};
let ws = &self.workspaces[idx];
ws.resize_edges_under(pos_within_output + ws_offset)
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.resize_edges_under(pos_within_output)
}
}
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() {
@@ -578,67 +875,88 @@ 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());
let output_transform = self.output.current_transform();
let output_mode = self.output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
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;
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,
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),
),
)?,
Point::from((0., -offset + size.h)).to_physical_precise_round(scale),
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)),
scale,
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, to_physical_precise_round(scale, size.h)),
),
)?,
(0, -offset),
Point::from((0., -offset)).to_physical_precise_round(scale),
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| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
scale,
// HACK: set infinite crop bounds due to a damage tracking bug
// which causes glitched rendering for maximized GTK windows.
// FIXME: use proper bounds after fixing the Crop element.
@@ -656,4 +974,112 @@ impl Monitor<Window> {
}
}
}
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
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(),
is_touchpad,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn workspace_switch_gesture_update(
&mut self,
delta_y: f64,
timestamp: Duration,
is_touchpad: bool,
) -> Option<bool> {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None;
};
if gesture.is_touchpad != is_touchpad {
return None;
}
gesture.tracker.push(delta_y, timestamp);
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
self.workspaces[0].view_size().h
};
let pos = gesture.tracker.pos() / total_height;
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,
is_touchpad: Option<bool>,
) -> bool {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
if is_touchpad.map_or(false, |x| gesture.is_touchpad != x) {
return false;
}
if cancelled {
self.workspace_switch = None;
self.clean_up_workspaces();
return true;
}
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
self.workspaces[0].view_size().h
};
let mut velocity = gesture.tracker.velocity() / total_height;
let current_pos = gesture.tracker.pos() / total_height;
let pos = gesture.tracker.projected_end_pos() / total_height;
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
}
}
+154
View File
@@ -0,0 +1,154 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
impl OpenAnimation {
pub fn new(anim: Animation) -> Self {
Self {
anim,
random_seed: fastrand::f32(),
}
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn is_done(&self) -> bool {
self.anim.is_done()
}
// We can't depend on view_rect here, because the result of window opening can be snapshot and
// then rendered elsewhere.
pub fn render(
&self,
renderer: &mut GlesRenderer,
elements: &[impl RenderElement<GlesRenderer>],
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
let mut target_size = area.size.upscale(1.5);
target_size.w = f64::max(area.size.w + 1000., target_size.w);
target_size.h = f64::max(area.size.h + 1000., target_size.h);
let diff = (target_size.to_point() - area.size.to_point()).downscale(2.);
let diff = diff.to_physical_precise_round(scale).to_logical(scale);
area.loc -= diff;
area.size += diff.upscale(2.).to_size();
let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32);
let area_size = Vec2::new(area.size.w as f32, area.size.h as f32);
let geo_loc = Vec2::new(location.x as f32, location.y as f32);
let geo_size = Vec2::new(geo_size.w as f32, geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = Vec2::new(scale.x as f32, scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return Ok(ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc)
.into());
}
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
(location + offset).to_physical_precise_round(scale),
Relocate::Relative,
);
Ok(elem.into())
}
}
+720 -96
View File
@@ -1,16 +1,28 @@
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::FocusRing;
use super::workspace::WorkspaceRenderElement;
use super::{LayoutElement, Options};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
use super::{
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
RESIZE_ANIMATION_THRESHOLD,
};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
/// 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,
@@ -31,56 +49,338 @@ pub struct Tile<W: LayoutElement> {
fullscreen_backdrop: SolidColorBuffer,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<i32, Logical>,
fullscreen_size: Size<f64, Logical>,
/// The animation upon opening a window.
open_animation: Option<OpenAnimation>,
/// 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>,
/// Snapshot of the last render for use in the close animation.
unmap_snapshot: Option<TileRenderSnapshot>,
/// Extra damage for clipped surface corner radius changes.
rounded_corner_damage: RoundedCornerDamage,
/// Scale of the output the tile is on (and rounds its sizes to).
scale: f64,
/// Configurable properties of the layout.
options: Rc<Options>,
pub options: Rc<Options>,
}
niri_render_elements! {
TileRenderElement<R> => {
LayoutElement = LayoutElementRenderElement<R>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Opening = OpeningWindowRenderElement,
Resize = ResizeRenderElement,
Border = BorderRenderElement,
ClippedSurface = ClippedSurfaceRenderElement<R>,
ExtraDamage = ExtraDamage,
}
}
type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
struct ResizeAnimation {
anim: Animation,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
}
#[derive(Debug)]
struct MoveAnimation {
anim: Animation,
from: f64,
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
let rules = window.rules();
let border_config = rules.border.resolve_against(options.border);
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
Self {
window,
border: FocusRing::new(options.border),
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config.into()),
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
fullscreen_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,
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
scale,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
self.border.update_config(options.border);
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
self.scale = scale;
self.options = options;
let rules = self.window.rules();
let border_config = rules.border.resolve_against(self.options.border);
self.border.update_config(border_config.into());
let focus_ring_config = rules
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
}
pub fn update_shaders(&mut self) {
self.border.update_shaders();
self.focus_ring.update_shaders();
}
pub fn update_window(&mut self) {
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0, 0)) {
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 + (size.w - size_from.w) * val;
size.h = size_from.h + (size.h - size_from.h) * val;
size
} else {
animate_from.size
};
let change = self.window.size().to_f64().to_point() - size_from.to_point();
let change = f64::max(change.x.abs(), change.y.abs());
if change > RESIZE_ANIMATION_THRESHOLD {
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
self.resize_animation = Some(ResizeAnimation {
anim,
size_from,
snapshot: animate_from,
});
} else {
self.resize_animation = None;
}
}
let rules = self.window.rules();
let border_config = rules.border.resolve_against(self.options.border);
self.border.update_config(border_config.into());
let focus_ring_config = rules
.focus_ring
.resolve_against(self.options.focus_ring.into());
self.focus_ring.update_config(focus_ring_config.into());
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
.unwrap_or_default()
.fit_to(window_size.w as f32, window_size.h as f32);
self.rounded_corner_damage.set_corner_radius(radius);
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(open) = &mut self.open_animation {
open.advance_animations(current_time);
if open.is_done() {
self.open_animation = None;
}
}
if let Some(resize) = &mut self.resize_animation {
resize.anim.set_current_time(current_time);
if resize.anim.is_done() {
self.resize_animation = None;
}
}
if let Some(move_) = &mut self.move_x_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_x_animation = None;
}
}
if let Some(move_) = &mut self.move_y_animation {
move_.anim.set_current_time(current_time);
if move_.anim.is_done() {
self.move_y_animation = None;
}
}
}
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 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 update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
let rules = self.window.rules();
let draw_border_with_background = rules
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
let border_width = self.effective_border_width().unwrap_or(0.);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else {
rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
})
};
self.border.update_render_elements(
self.animated_window_size(),
is_active,
!draw_border_with_background,
Rectangle::from_loc_and_size(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
),
radius,
self.scale,
);
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
};
let radius = if self.is_fullscreen {
CornerRadius::default()
} else if self.effective_border_width().is_some() {
radius
} else {
rules.geometry_corner_radius.unwrap_or_default()
}
.expanded_by(self.focus_ring.width() as f32);
self.focus_ring.update_render_elements(
self.animated_tile_size(),
is_active,
!draw_focus_ring_with_background,
view_rect,
radius,
self.scale,
);
}
pub fn scale(&self) -> f64 {
self.scale
}
pub fn render_offset(&self) -> Point<f64, Logical> {
let mut offset = Point::from((0., 0.));
if let Some(move_) = &self.move_x_animation {
offset.x += move_.from * move_.anim.value();
}
if let Some(move_) = &self.move_y_animation {
offset.y += move_.from * move_.anim.value();
}
offset
}
pub fn start_open_animation(&mut self) {
self.open_animation = Some(OpenAnimation::new(Animation::new(
0.,
1.,
0.,
self.options.animations.window_open.anim,
)));
}
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<f64, Logical>) {
self.animate_move_x_from(from.x);
self.animate_move_y_from(from.y);
}
pub fn animate_move_x_from(&mut self, from: f64) {
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: f64, 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: f64) {
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: f64, 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> {
fn effective_border_width(&self) -> Option<f64> {
if self.is_fullscreen {
return None;
}
@@ -93,22 +393,27 @@ impl<W: LayoutElement> Tile<W> {
}
/// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
pub fn window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window.size();
let window_size = self.window_size();
let target_size = self.fullscreen_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
if window_size.w < target_size.w {
loc.x += (target_size.w - window_size.w) / 2;
loc.x += (target_size.w - window_size.w) / 2.;
}
if window_size.h < target_size.h {
loc.y += (target_size.h - window_size.h) / 2;
loc.y += (target_size.h - window_size.h) / 2.;
}
// Round to physical pixels.
loc = loc
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
}
if let Some(width) = self.effective_border_width() {
@@ -118,33 +423,73 @@ impl<W: LayoutElement> Tile<W> {
loc
}
pub fn tile_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
pub fn tile_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = max(size.w, self.fullscreen_size.w);
size.h = max(size.h, self.fullscreen_size.h);
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::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.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn window_size(&self) -> Size<i32, Logical> {
self.window.size()
pub fn window_size(&self) -> Size<f64, Logical> {
let mut size = self.window.size().to_f64();
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
size
}
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
fn animated_window_size(&self) -> Size<f64, 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.to_f64();
size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val);
size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val);
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
}
size
}
fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.fullscreen_size.w);
size.h = f64::max(size.h, self.fullscreen_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn buf_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
loc += self.window_loc();
loc += self.window.buf_loc();
loc += self.window.buf_loc().to_f64();
loc
}
@@ -154,124 +499,403 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0, 0), self.tile_size());
activation_region.to_f64().contains(point)
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
activation_region.contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
pub fn request_tile_size(&mut self, mut size: Size<f64, 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();
size.w = max(1, size.w - width * 2);
size.h = max(1, size.h - width * 2);
size.w = f64::max(1., size.w - width * 2.);
size.h = f64::max(1., size.h - width * 2.);
}
self.window.request_size(size);
// The size request has to be i32 unfortunately, due to Wayland. We floor here instead of
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window.request_size(size.to_i32_floor(), animate);
}
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
size + self.border.width() * 2.
}
}
pub fn tile_height_for_window_height(&self, size: i32) -> i32 {
pub fn tile_height_for_window_height(&self, size: f64) -> f64 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
size + self.border.width() * 2.
}
}
pub fn window_height_for_tile_height(&self, size: i32) -> i32 {
pub fn window_width_for_tile_width(&self, size: f64) -> f64 {
if self.border.is_off() {
size
} else {
size.saturating_sub(self.border.width() * 2)
size - self.border.width() * 2.
}
}
pub fn request_fullscreen(&mut self, size: Size<i32, Logical>) {
pub fn window_height_for_tile_height(&self, size: f64) -> f64 {
if self.border.is_off() {
size
} else {
size - self.border.width() * 2.
}
}
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size);
self.window.request_fullscreen(size.to_i32_round());
}
pub fn min_size(&self) -> Size<i32, Logical> {
let mut size = self.window.min_size();
pub fn min_size(&self) -> Size<f64, Logical> {
let mut size = self.window.min_size().to_f64();
if let Some(width) = self.effective_border_width() {
size.w = max(1, size.w);
size.h = max(1, size.h);
size.w = f64::max(1., size.w);
size.h = f64::max(1., size.h);
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
size.w += width * 2.;
size.h += width * 2.;
}
size
}
pub fn max_size(&self) -> Size<i32, Logical> {
let mut size = self.window.max_size();
pub fn max_size(&self) -> Size<f64, Logical> {
let mut size = self.window.max_size().to_f64();
if let Some(width) = self.effective_border_width() {
if size.w > 0 {
size.w = size.w.saturating_add(width * 2);
if size.w > 0. {
size.w += width * 2.;
}
if size.h > 0 {
size.h = size.h.saturating_add(width * 2);
if size.h > 0. {
size.h += width * 2.;
}
}
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>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> Vec<WorkspaceRenderElement<R>>
where
<R as Renderer>::TextureId: 'static,
{
let mut rv = Vec::new();
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
let _span = tracy_client::span!("Tile::render_inner");
let window_pos = location + self.window_loc();
rv.extend(self.window.render(renderer, window_pos, scale));
let alpha = if self.is_fullscreen {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
if self.effective_border_width().is_some() {
rv.extend(
self.border
.render(scale)
.map(|elem| {
RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
Relocate::Relative,
)
})
let window_loc = self.window_loc();
let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
let rules = self.window.rules();
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
let radius = rules.geometry_corner_radius.unwrap_or_default();
// If we're resizing, try to render a shader, or a fallback.
let mut resize_shader = None;
let mut resize_popups = None;
let mut resize_fallback = None;
if let Some(resize) = &self.resize_animation {
resize_popups = Some(
self.window
.render_popups(renderer, window_render_loc, scale, alpha, target)
.into_iter()
.map(Into::into),
);
if ResizeRenderElement::has_shader(renderer) {
let gles_renderer = renderer.as_gles_renderer();
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
let window_elements = self.window.render_normal(
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();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
let clip_to_geometry = if target
.should_block_out(resize.snapshot.block_out_from)
&& target.should_block_out(rules.block_out_from)
{
true
} else {
clip_to_geometry
};
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
let elem = ResizeRenderElement::new(
area,
scale,
texture_from.clone(),
resize.snapshot.size,
(texture_current, texture_current_geo),
window_size,
resize.anim.value() as f32,
resize.anim.clamped_value().clamp(0., 1.) as f32,
radius,
clip_to_geometry,
alpha,
);
// FIXME: with split popups, this will use the resize element ID for
// popups, but we want the real IDs.
self.window
.set_offscreen_element_id(Some(elem.id().clone()));
resize_shader = Some(elem.into());
}
}
}
if resize_shader.is_none() {
let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
resize_fallback = Some(
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc,
alpha,
Kind::Unspecified,
)
.into(),
);
self.window.set_offscreen_element_id(None);
}
}
if self.is_fullscreen {
let elem = SolidColorRenderElement::from_buffer(
// If we're not resizing, render the window itself.
let mut window_surface = None;
let mut window_popups = None;
let mut rounded_corner_damage = None;
if resize_shader.is_none() && resize_fallback.is_none() {
let window = self
.window
.render(renderer, window_render_loc, scale, alpha, target);
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
let has_border_shader = BorderRenderElement::has_shader(renderer);
if clip_to_geometry && clip_shader.is_some() {
let damage = self.rounded_corner_damage.element();
rounded_corner_damage = Some(damage.with_location(window_render_loc).into());
}
window_surface = Some(window.normal.into_iter().map(move |elem| match elem {
LayoutElementRenderElement::Wayland(elem) => {
// If we should clip to geometry, render a clipped window.
if clip_to_geometry {
if let Some(shader) = clip_shader.clone() {
if ClippedSurfaceRenderElement::will_clip(&elem, scale, geo, radius) {
return ClippedSurfaceRenderElement::new(
elem,
scale,
geo,
shader.clone(),
radius,
)
.into();
}
}
}
// Otherwise, render it normally.
LayoutElementRenderElement::Wayland(elem).into()
}
LayoutElementRenderElement::SolidColor(elem) => {
// In this branch we're rendering a blocked-out window with a solid
// color. We need to render it with a rounded corner shader even if
// clip_to_geometry is false, because in this case we're assuming that
// the unclipped window CSD already has corners rounded to the
// user-provided radius, so our blocked-out rendering should match that
// radius.
if radius != CornerRadius::default() && has_border_shader {
return BorderRenderElement::new(
geo.size,
Rectangle::from_loc_and_size((0., 0.), geo.size),
GradientInterpolation::default(),
Color::from_array_premul(elem.color()),
Color::from_array_premul(elem.color()),
0.,
Rectangle::from_loc_and_size((0., 0.), geo.size),
0.,
radius,
scale.x as f32,
)
.with_location(geo.loc)
.into();
}
// Otherwise, render the solid color as is.
LayoutElementRenderElement::SolidColor(elem).into()
}
}));
window_popups = Some(window.popups.into_iter().map(Into::into));
}
let rv = resize_popups
.into_iter()
.flatten()
.chain(resize_shader)
.chain(resize_fallback)
.chain(window_popups.into_iter().flatten())
.chain(rounded_corner_damage)
.chain(window_surface.into_iter().flatten());
let elem = self.is_fullscreen.then(|| {
SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
location,
1.,
Kind::Unspecified,
);
rv.push(elem.into());
)
.into()
});
let rv = rv.chain(elem);
let elem = self.effective_border_width().map(|width| {
self.border
.render(renderer, location + Point::from((width, width)))
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
rv.chain(elem.into_iter().flatten())
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
let _span = tracy_client::span!("Tile::render");
let mut open_anim_elem = None;
let mut window_elems = None;
if let Some(open) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements =
self.render_inner(renderer, Point::from((0., 0.)), scale, focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match open.render(renderer, &elements, self.tile_size(), location, scale) {
Ok(elem) => {
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
open_anim_elem = Some(elem.into());
}
Err(err) => {
warn!("error rendering window opening animation: {err:?}");
}
}
}
rv
if open_anim_elem.is_none() {
self.window().set_offscreen_element_id(None);
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
}
open_anim_elem
.into_iter()
.chain(window_elems.into_iter().flatten())
}
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
}
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
scale: Scale<f64>,
) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
);
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let blocked_out_contents = self.render(
renderer,
Point::from((0., 0.)),
scale,
false,
RenderTarget::Screencast,
);
RenderSnapshot {
contents: contents.collect(),
blocked_out_contents: blocked_out_contents.collect(),
block_out_from: self.window.rules().block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
blocked_out_texture: Default::default(),
}
}
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
self.unmap_snapshot.take()
}
}
+2489 -586
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
#[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 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;
+157 -110
View File
@@ -1,95 +1,36 @@
#[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, store_and_increase_nofile_rlimit, 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,
}
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
@@ -102,33 +43,44 @@ 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);
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
#[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(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.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");
}
// 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 cli = Cli::parse();
let _client = tracy_client::Client::start();
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
@@ -136,7 +88,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
tracy_client::Client::start();
let path = config
.or_else(env_config_path)
.or_else(default_config_path)
.expect("error getting config path");
Config::load(&path)?;
@@ -151,10 +106,53 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Avoid starting Tracy for the `niri msg` code path since starting/stopping Tracy is a bit
// slow.
tracy_client::Client::start();
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(env_config_path);
env::remove_var("NIRI_CONFIG");
let path = path.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 +167,17 @@ 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);
store_and_increase_nofile_rlimit();
// Create the compositor.
let mut event_loop = EventLoop::try_new().unwrap();
@@ -180,7 +187,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 +204,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 +218,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 +256,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 +268,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,11 +313,17 @@ 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:?}");
}
}
}
fn env_config_path() -> Option<PathBuf> {
env::var_os("NIRI_CONFIG")
.filter(|x| !x.is_empty())
.map(PathBuf::from)
}
fn default_config_path() -> Option<PathBuf> {
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
warn!("error retrieving home directory");
@@ -299,3 +334,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(())
}
+2308 -810
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);
};
}
+4
View File
@@ -0,0 +1,4 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod output_management;
pub mod screencopy;
+889
View File
@@ -0,0 +1,889 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::iter::zip;
use std::mem;
use niri_config::FloatOrInt;
use niri_ipc::Transform;
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
zwlr_output_manager_v1, zwlr_output_mode_v1,
};
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::Transform as WlTransform;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
};
use zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1;
use zwlr_output_configuration_v1::ZwlrOutputConfigurationV1;
use zwlr_output_head_v1::{AdaptiveSyncState, ZwlrOutputHeadV1};
use zwlr_output_manager_v1::ZwlrOutputManagerV1;
use zwlr_output_mode_v1::ZwlrOutputModeV1;
use crate::backend::OutputId;
use crate::niri::State;
use crate::utils::ipc_transform_to_smithay;
const VERSION: u32 = 4;
#[derive(Debug)]
struct ClientData {
heads: HashMap<OutputId, (ZwlrOutputHeadV1, Vec<ZwlrOutputModeV1>)>,
confs: HashMap<ZwlrOutputConfigurationV1, OutputConfigurationState>,
manager: ZwlrOutputManagerV1,
}
pub struct OutputManagementManagerState {
display: DisplayHandle,
serial: u32,
clients: HashMap<ClientId, ClientData>,
current_state: HashMap<OutputId, niri_ipc::Output>,
current_config: niri_config::Outputs,
}
pub struct OutputManagementManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait OutputManagementHandler {
fn output_management_state(&mut self) -> &mut OutputManagementManagerState;
fn apply_output_config(&mut self, config: niri_config::Outputs);
}
#[derive(Debug)]
enum OutputConfigurationState {
Ongoing(HashMap<OutputId, niri_config::Output>),
Finished,
}
pub enum OutputConfigurationHeadState {
Cancelled,
Ok(OutputId, ZwlrOutputConfigurationV1),
}
impl OutputManagementManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = OutputManagementManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrOutputManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
clients: HashMap::new(),
serial: 0,
current_state: HashMap::new(),
current_config: Default::default(),
}
}
pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) {
self.current_config = new_config;
}
pub fn notify_changes(&mut self, new_state: HashMap<OutputId, niri_ipc::Output>) {
let mut changed = false; /* most likely to end up true */
for (output, conf) in new_state.iter() {
if let Some(old) = self.current_state.get(output) {
if old.vrr_enabled != conf.vrr_enabled {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
head.adaptive_sync(match conf.vrr_enabled {
true => AdaptiveSyncState::Enabled,
false => AdaptiveSyncState::Disabled,
});
}
}
}
}
// TTY outputs can't change modes I think, however, winit and virtual outputs can.
let modes_changed = old.modes != conf.modes;
if modes_changed {
changed = true;
if old.modes.len() != conf.modes.len() {
error!("output's old mode count doesn't match new modes");
} else {
for client in self.clients.values() {
if let Some((_, modes)) = client.heads.get(output) {
for (wl_mode, mode) in zip(modes, &conf.modes) {
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
wl_mode.refresh(refresh_rate);
}
}
}
}
}
}
match (old.current_mode, conf.current_mode) {
(Some(old_index), Some(new_index)) => {
if old.modes.len() == conf.modes.len()
&& (modes_changed || old_index != new_index)
{
changed = true;
for client in self.clients.values() {
if let Some((head, modes)) = client.heads.get(output) {
if let Some(new_mode) = modes.get(new_index) {
head.current_mode(new_mode);
} else {
error!(
"output new mode doesnt exist for the client's output"
);
}
}
}
}
}
(Some(_), None) => {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
head.enabled(0);
}
}
}
(None, Some(new_index)) => {
if old.modes.len() == conf.modes.len() {
changed = true;
for client in self.clients.values() {
if let Some((head, modes)) = client.heads.get(output) {
head.enabled(1);
if let Some(mode) = modes.get(new_index) {
head.current_mode(mode);
} else {
error!(
"output new mode doesnt exist for the client's output"
);
}
}
}
}
}
(None, None) => {}
}
match (old.logical, conf.logical) {
(Some(old_logical), Some(new_logical)) => {
if old_logical != new_logical {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
if old_logical.x != new_logical.x
|| old_logical.y != new_logical.y
{
head.position(new_logical.x, new_logical.y);
}
if old_logical.scale != new_logical.scale {
head.scale(new_logical.scale);
}
if old_logical.transform != new_logical.transform {
head.transform(
ipc_transform_to_smithay(new_logical.transform).into(),
);
}
}
}
}
}
(None, Some(new_logical)) => {
changed = true;
for client in self.clients.values() {
if let Some((head, _)) = client.heads.get(output) {
// head enable in the mode diff check
head.position(new_logical.x, new_logical.y);
head.transform(
ipc_transform_to_smithay(new_logical.transform).into(),
);
head.scale(new_logical.scale);
}
}
}
(Some(_), None) => {
// heads disabled in the mode diff check
}
(None, None) => {}
}
} else {
changed = true;
notify_new_head(self, output, conf);
}
}
for (old, _) in self.current_state.iter() {
if !new_state.contains_key(old) {
changed = true;
notify_removed_head(&mut self.clients, old);
}
}
if changed {
self.current_state = new_state;
self.serial += 1;
for data in self.clients.values() {
data.manager.done(self.serial);
for conf in data.confs.keys() {
conf.cancelled();
}
}
}
}
}
impl<D> GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData, D>
for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn bind(
state: &mut D,
display: &DisplayHandle,
client: &Client,
manager: New<ZwlrOutputManagerV1>,
_manager_state: &OutputManagementManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(manager, ());
let g_state = state.output_management_state();
let mut client_data = ClientData {
heads: HashMap::new(),
confs: HashMap::new(),
manager: manager.clone(),
};
for (output, conf) in &g_state.current_state {
send_new_head::<D>(display, client, &mut client_data, *output, conf);
}
g_state.clients.insert(client.id(), client_data);
manager.done(g_state.serial);
}
fn can_view(client: Client, global_data: &OutputManagementManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrOutputManagerV1, (), D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
_manager: &ZwlrOutputManagerV1,
request: zwlr_output_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => {
let g_state = state.output_management_state();
let conf = data_init.init(id, serial);
if let Some(client_data) = g_state.clients.get_mut(&client.id()) {
if serial != g_state.serial {
conf.cancelled();
}
let state = OutputConfigurationState::Ongoing(HashMap::new());
client_data.confs.insert(conf, state);
} else {
error!("CreateConfiguration: missing client data");
}
}
zwlr_output_manager_v1::Request::Stop => {
if let Some(c) = state.output_management_state().clients.remove(&client.id()) {
c.manager.finished()
}
}
_ => unreachable!(),
}
}
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputManagerV1, _data: &()) {
state.output_management_state().clients.remove(&client);
}
}
impl<D> Dispatch<ZwlrOutputConfigurationV1, u32, D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
conf: &ZwlrOutputConfigurationV1,
request: zwlr_output_configuration_v1::Request,
serial: &u32,
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let g_state = state.output_management_state();
let outdated = *serial != g_state.serial;
if outdated {
debug!("OutputConfiguration: request from an outdated configuration");
}
let new_config = g_state
.clients
.get_mut(&client.id())
.and_then(|data| data.confs.get_mut(conf));
if new_config.is_none() {
error!("OutputConfiguration: request from unknown configuration object");
}
match request {
zwlr_output_configuration_v1::Request::EnableHead { id, head } => {
let Some(output) = head.data::<OutputId>() else {
error!("EnableHead: Missing attached output");
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
if outdated {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
}
let Some(new_config) = new_config else {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(current_config) = g_state.current_state.get(output) else {
error!("EnableHead: output missing from current config");
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
return;
};
match new_config.entry(*output) {
Entry::Occupied(_) => {
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
"head has been already configured",
);
return;
}
Entry::Vacant(entry) => {
let mut config = g_state
.current_config
.find(&current_config.name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
..Default::default()
});
config.off = false;
entry.insert(config);
}
};
data_init.init(id, OutputConfigurationHeadState::Ok(*output, conf.clone()));
}
zwlr_output_configuration_v1::Request::DisableHead { head } => {
if outdated {
return;
}
let Some(output) = head.data::<OutputId>() else {
error!("DisableHead: missing attached output head name");
return;
};
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(current_config) = g_state.current_state.get(output) else {
error!("EnableHead: output missing from current config");
return;
};
match new_config.entry(*output) {
Entry::Occupied(_) => {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
"head has been already configured",
);
}
Entry::Vacant(entry) => {
let mut config = g_state
.current_config
.find(&current_config.name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
..Default::default()
});
config.off = true;
entry.insert(config);
}
};
}
zwlr_output_configuration_v1::Request::Apply => {
if outdated {
conf.cancelled();
return;
}
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) =
mem::replace(new_config, OutputConfigurationState::Finished)
else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let any_enabled = new_config.values().any(|c| !c.off);
if !any_enabled {
conf.failed();
return;
}
state.apply_output_config(new_config.into_values().collect());
// FIXME: verify that it had been applied successfully (which may be difficult).
conf.succeeded();
}
zwlr_output_configuration_v1::Request::Test => {
if outdated {
conf.cancelled();
return;
}
let Some(new_config) = new_config else {
return;
};
let OutputConfigurationState::Ongoing(new_config) =
mem::replace(new_config, OutputConfigurationState::Finished)
else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let any_enabled = new_config.values().any(|c| !c.off);
if !any_enabled {
conf.failed();
return;
}
// FIXME: actually test the configuration with TTY.
conf.succeeded()
}
zwlr_output_configuration_v1::Request::Destroy => {
g_state
.clients
.get_mut(&client.id())
.map(|d| d.confs.remove(conf));
}
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState, D>
for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
state: &mut D,
client: &Client,
conf_head: &ZwlrOutputConfigurationHeadV1,
request: zwlr_output_configuration_head_v1::Request,
data: &OutputConfigurationHeadState,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let g_state = state.output_management_state();
let Some(client_data) = g_state.clients.get_mut(&client.id()) else {
error!("ConfigurationHead: missing client data");
return;
};
let OutputConfigurationHeadState::Ok(output_id, conf) = data else {
warn!("ConfigurationHead: request sent to a cancelled head");
return;
};
let Some(serial) = conf.data::<u32>() else {
error!("ConfigurationHead: missing serial");
return;
};
if *serial != g_state.serial {
warn!("ConfigurationHead: request sent to an outdated");
return;
}
let Some(new_config) = client_data.confs.get_mut(conf) else {
error!("ConfigurationHead: unknown configuration");
return;
};
let OutputConfigurationState::Ongoing(new_config) = new_config else {
conf.post_error(
zwlr_output_configuration_v1::Error::AlreadyUsed,
"configuration had already been used",
);
return;
};
let Some(new_config) = new_config.get_mut(output_id) else {
error!("ConfigurationHead: config missing from enabled heads");
return;
};
match request {
zwlr_output_configuration_head_v1::Request::SetMode { mode } => {
let index = match client_data
.heads
.get(output_id)
.map(|(_, mods)| mods.iter().position(|m| m.id() == mode.id()))
{
Some(Some(index)) => index,
_ => {
warn!("SetMode: failed to find requested mode");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidMode,
"failed to find requested mode",
);
return;
}
};
let Some(current_config) = g_state.current_state.get(output_id) else {
warn!("SetMode: output missing from the current config");
return;
};
let Some(mode) = current_config.modes.get(index) else {
error!("SetMode: requested mode is out of range");
return;
};
new_config.mode = Some(niri_ipc::ConfiguredMode {
width: mode.width,
height: mode.height,
refresh: Some(mode.refresh_rate as f64 / 1000.),
});
}
zwlr_output_configuration_head_v1::Request::SetCustomMode {
width,
height,
refresh,
} => {
// FIXME: Support custom mode
let (width, height, refresh): (u16, u16, u32) =
match (width.try_into(), height.try_into(), refresh.try_into()) {
(Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh),
_ => {
warn!("SetCustomMode: invalid input data");
return;
}
};
let Some(current_config) = g_state.current_state.get(output_id) else {
warn!("SetMode: output missing from the current config");
return;
};
let Some(mode) = current_config.modes.iter().find(|m| {
m.width == width
&& m.height == height
&& (refresh == 0 || m.refresh_rate == refresh)
}) else {
warn!("SetCustomMode: no matching mode");
return;
};
new_config.mode = Some(niri_ipc::ConfiguredMode {
width: mode.width,
height: mode.height,
refresh: Some(mode.refresh_rate as f64 / 1000.),
});
}
zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => {
new_config.position = Some(niri_config::Position { x, y });
}
zwlr_output_configuration_head_v1::Request::SetTransform { transform } => {
let transform = match transform {
WEnum::Value(WlTransform::Normal) => Transform::Normal,
WEnum::Value(WlTransform::_90) => Transform::_90,
WEnum::Value(WlTransform::_180) => Transform::_180,
WEnum::Value(WlTransform::_270) => Transform::_270,
WEnum::Value(WlTransform::Flipped) => Transform::Flipped,
WEnum::Value(WlTransform::Flipped90) => Transform::Flipped90,
WEnum::Value(WlTransform::Flipped180) => Transform::Flipped180,
WEnum::Value(WlTransform::Flipped270) => Transform::Flipped270,
_ => {
warn!("SetTransform: unknown requested transform");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidTransform,
"unknown transform value",
);
return;
}
};
new_config.transform = transform;
}
zwlr_output_configuration_head_v1::Request::SetScale { scale } => {
if scale <= 0. {
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidScale,
"scale is negative or zero",
);
return;
}
new_config.scale = Some(FloatOrInt(scale));
}
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
let enabled = match state {
WEnum::Value(AdaptiveSyncState::Enabled) => true,
WEnum::Value(AdaptiveSyncState::Disabled) => false,
_ => {
warn!("SetAdaptativeSync: unknown requested adaptative sync");
conf_head.post_error(
zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState,
"unknown adaptive sync value",
);
return;
}
};
new_config.variable_refresh_rate = enabled;
}
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrOutputHeadV1, OutputId, D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_output_head: &ZwlrOutputHeadV1,
request: zwlr_output_head_v1::Request,
_data: &OutputId,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_head_v1::Request::Release => {}
_ => unreachable!(),
}
}
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputHeadV1, data: &OutputId) {
if let Some(c) = state.output_management_state().clients.get_mut(&client) {
c.heads.remove(data);
}
}
}
impl<D> Dispatch<ZwlrOutputModeV1, (), D> for OutputManagementManagerState
where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_mode: &ZwlrOutputModeV1,
request: zwlr_output_mode_v1::Request,
_data: &(),
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_output_mode_v1::Request::Release => {}
_ => unreachable!(),
}
}
}
#[macro_export]
macro_rules! delegate_output_management{
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocols::output_management::OutputManagementManagerGlobalData
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: ()
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: u32
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: $crate::backend::OutputId
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: ()
] => $crate::protocols::output_management::OutputManagementManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::protocols::output_management::OutputConfigurationHeadState
] => $crate::protocols::output_management::OutputManagementManagerState);
};
}
fn notify_removed_head(clients: &mut HashMap<ClientId, ClientData>, head: &OutputId) {
for data in clients.values_mut() {
if let Some((head, mods)) = data.heads.remove(head) {
mods.iter().for_each(|m| m.finished());
head.finished();
}
}
}
fn notify_new_head(
state: &mut OutputManagementManagerState,
output: &OutputId,
conf: &niri_ipc::Output,
) {
let display = &state.display;
let clients = &mut state.clients;
for data in clients.values_mut() {
if let Some(client) = data.manager.client() {
send_new_head::<State>(display, &client, data, *output, conf);
}
}
}
fn send_new_head<D>(
display: &DisplayHandle,
client: &Client,
client_data: &mut ClientData,
output: OutputId,
conf: &niri_ipc::Output,
) where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: 'static,
{
let new_head = client
.create_resource::<ZwlrOutputHeadV1, _, D>(display, client_data.manager.version(), output)
.unwrap();
client_data.manager.head(&new_head);
new_head.name(conf.name.clone());
new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name));
if let Some((width, height)) = conf.physical_size {
if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) {
new_head.physical_size(a, b);
}
}
let mut new_modes = Vec::with_capacity(conf.modes.len());
for (index, mode) in conf.modes.iter().enumerate() {
let new_mode = client
.create_resource::<ZwlrOutputModeV1, _, D>(display, new_head.version(), ())
.unwrap();
new_head.mode(&new_mode);
new_mode.size(i32::from(mode.width), i32::from(mode.height));
if mode.is_preferred {
new_mode.preferred();
}
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
new_mode.refresh(refresh_rate);
}
if Some(index) == conf.current_mode {
new_head.current_mode(&new_mode);
}
new_modes.push(new_mode);
}
if let Some(logical) = conf.logical {
new_head.position(logical.x, logical.y);
new_head.transform(ipc_transform_to_smithay(logical.transform).into());
new_head.scale(logical.scale);
}
new_head.enabled(conf.current_mode.is_some() as i32);
if new_head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE {
new_head.make(conf.make.clone());
}
if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE {
new_head.model(conf.model.clone());
}
if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
new_head.adaptive_sync(match conf.vrr_enabled {
true => AdaptiveSyncState::Enabled,
false => AdaptiveSyncState::Disabled,
});
}
// new_head.serial_number(output.serial);
client_data.heads.insert(output, (new_head, new_modes));
}
+508
View File
@@ -0,0 +1,508 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use calloop::generic::Generic;
use calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::{Buffer, Fourcc};
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::sync::SyncPoint;
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::Format;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use smithay::wayland::{dmabuf, shm};
use crate::utils::get_monotonic_time;
const VERSION: u32 = 3;
pub struct ScreencopyQueue {
damage_tracker: OutputDamageTracker,
screencopies: Vec<Screencopy>,
}
impl Default for ScreencopyQueue {
fn default() -> Self {
Self::new()
}
}
impl ScreencopyQueue {
pub fn new() -> Self {
Self {
damage_tracker: OutputDamageTracker::new((0, 0), 1.0, Transform::Normal),
screencopies: Vec::new(),
}
}
pub fn split(&mut self) -> (&mut OutputDamageTracker, Option<&Screencopy>) {
let ScreencopyQueue {
damage_tracker,
screencopies,
} = self;
(damage_tracker, screencopies.first())
}
pub fn push(&mut self, screencopy: Screencopy) {
self.screencopies.push(screencopy);
}
pub fn pop(&mut self) -> Screencopy {
self.screencopies.pop().unwrap()
}
pub fn remove_output(&mut self, output: &Output) {
self.screencopies
.retain(|screencopy| screencopy.output() != output);
}
}
#[derive(Default)]
pub struct ScreencopyManagerState {
queues: HashMap<ZwlrScreencopyManagerV1, ScreencopyQueue>,
}
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 {
queues: HashMap::new(),
}
}
pub fn bind(&mut self, manager: &ZwlrScreencopyManagerV1) {
// Clean up all entries if its manager is dead and its queue is empty.
self.queues
.retain(|k, v| k.is_alive() || !v.screencopies.is_empty());
self.queues.insert(manager.clone(), ScreencopyQueue::new());
}
pub fn get_queue_mut(
&mut self,
manager: &ZwlrScreencopyManagerV1,
) -> Option<&mut ScreencopyQueue> {
self.queues.get_mut(manager)
}
pub fn queues_mut(&mut self) -> impl Iterator<Item = &mut ScreencopyQueue> {
self.queues.values_mut()
}
}
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>,
) {
let manager = data_init.init(manager, ());
state.screencopy_state().bind(&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 Some(output) = Output::from_resource(&output) else {
trace!("screencopy client requested non-existent output");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
};
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 Some(output) = Output::from_resource(&output) else {
trace!("screencopy client requested non-existent output");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
};
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().fractional_scale();
let physical_rect = rect.to_physical_precise_round(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 {
manager: manager.clone(),
info,
copied: Arc::new(AtomicBool::new(false)),
},
);
// Send desired SHM buffer parameters.
frame.buffer(
Format::Xrgb8888,
buffer_size.w as u32,
buffer_size.h as u32,
buffer_size.w as u32 * 4,
);
if frame.version() >= 3 {
// Send desired DMA buffer parameters.
frame.linux_dmabuf(
Fourcc::Xrgb8888 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, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy);
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState;
}
#[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 {
manager: ZwlrScreencopyManagerV1,
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 ScreencopyFrameState::Pending {
manager,
info,
copied,
} = data
else {
return;
};
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!(),
};
let size = info.buffer_size;
let buffer = if let Ok(dmabuf) = dmabuf::get_dmabuf(&buffer) {
if dmabuf.format().code == Fourcc::Xrgb8888
&& dmabuf.width() == size.w as u32
&& dmabuf.height() == size.h as u32
{
ScreencopyBuffer::Dmabuf(dmabuf.clone())
} else {
frame.post_error(
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
"invalid dmabuf parameters",
);
return;
}
} else if shm::with_buffer_contents(&buffer, |_, shm_len, buffer_data| {
buffer_data.format == Format::Xrgb8888
&& buffer_data.width == size.w
&& buffer_data.height == size.h
&& buffer_data.stride == size.w * 4
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize
})
.unwrap_or(false)
{
ScreencopyBuffer::Shm(buffer)
} else {
frame.post_error(
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
"invalid buffer",
);
return;
};
copied.store(true, Ordering::SeqCst);
state.frame(
manager,
Screencopy {
buffer,
frame: frame.clone(),
info: info.clone(),
with_damage,
submitted: false,
},
);
}
}
/// Screencopy buffer.
#[derive(Clone)]
pub enum ScreencopyBuffer {
Dmabuf(Dmabuf),
Shm(WlBuffer),
}
/// Screencopy frame.
pub struct Screencopy {
info: ScreencopyFrameInfo,
frame: ZwlrScreencopyFrameV1,
buffer: ScreencopyBuffer,
with_damage: bool,
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) -> &ScreencopyBuffer {
&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 with_damage(&self) -> bool {
self.with_damage
}
pub fn damage(&self, damages: impl Iterator<Item = Rectangle<i32, smithay::utils::Buffer>>) {
for Rectangle { loc, size } in damages {
self.frame
.damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
}
}
/// Submit the copied content.
fn submit(mut self, y_invert: bool, timestamp: Duration) {
// Notify client that buffer is ordinary.
self.frame.flags(if y_invert {
Flags::YInvert
} else {
Flags::empty()
});
// Notify client about successful copy.
let tv_sec_hi = (timestamp.as_secs() >> 32) as u32;
let tv_sec_lo = (timestamp.as_secs() & 0xFFFFFFFF) as u32;
let tv_nsec = timestamp.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<SyncPoint>,
event_loop: &LoopHandle<'_, T>,
) {
let timestamp = get_monotonic_time();
match sync_point.and_then(|s| s.export()) {
None => self.submit(y_invert, timestamp),
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, timestamp);
Ok(PostAction::Remove)
})
.unwrap();
}
}
}
}
+665 -117
View File
@@ -1,57 +1,131 @@
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::io::Cursor;
use std::iter::zip;
use std::mem;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
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::deserialize::PodDeserializer;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
use pipewire::spa::pod::{self, ChoiceValue, Pod, PodPropFlags, 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::spa::{self};
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::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::{Format, Fourcc};
use smithay::backend::drm::DrmDeviceFd;
use smithay::output::Output;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::WeakOutput;
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, Scale, Size, Transform};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State;
use crate::render_helpers::render_to_dmabuf;
pub struct PipeWire {
_context: Context<MainLoop>,
_context: Context,
pub core: Core,
to_niri: calloop::channel::Sender<PwToNiri>,
}
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw(CastTarget),
}
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 target: CastTarget,
formats: FormatSet,
state: Rc<RefCell<CastState>>,
refresh: Rc<Cell<u32>>,
offer_alpha: bool,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>,
min_time_between_frames: Rc<Cell<Duration>>,
dmabufs: Rc<RefCell<HashMap<i64, Dmabuf>>>,
}
#[derive(Debug)]
pub enum CastState {
ResizePending {
pending_size: Size<u32, Physical>,
},
ConfirmationPending {
size: Size<u32, Physical>,
alpha: bool,
modifier: Modifier,
plane_count: i32,
},
Ready {
size: Size<u32, Physical>,
alpha: bool,
modifier: Modifier,
plane_count: i32,
},
}
#[derive(PartialEq, Eq)]
pub enum CastSizeChange {
Ready,
Pending,
}
#[derive(Clone, PartialEq, Eq)]
pub enum CastTarget {
Output(WeakOutput),
Window { id: u64 },
}
macro_rules! make_params {
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
let mut b1 = Vec::new();
let mut b2 = Vec::new();
let o1 = make_video_params($formats, $size, $refresh, false);
let pod1 = make_pod(&mut b1, o1);
let mut p1;
let mut p2;
$params = if $alpha {
let o2 = make_video_params($formats, $size, $refresh, true);
p2 = [pod1, make_pod(&mut b2, o2)];
&mut p2[..]
} else {
p1 = [pod1];
&mut p1[..]
};
};
}
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,71 +140,82 @@ 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();
let (to_niri, from_pipewire) = calloop::channel::channel();
event_loop
.insert_source(from_pipewire, move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
calloop::channel::Event::Closed => (),
})
.unwrap();
Ok(Self {
_context: context,
core,
to_niri,
})
}
#[allow(clippy::too_many_arguments)]
pub fn start_cast(
&self,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
gbm: GbmDevice<DrmDeviceFd>,
formats: FormatSet,
session_id: usize,
output: Output,
target: CastTarget,
size: Size<i32, Physical>,
refresh: u32,
alpha: bool,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
let to_niri_ = to_niri.clone();
let to_niri_ = self.to_niri.clone();
let stop_cast = move || {
if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) {
if let Err(err) = to_niri_.send(PwToNiri::StopCast { session_id }) {
warn!("error sending StopCast to niri: {err:?}");
}
};
let weak = output.downgrade();
let target_ = target.clone();
let to_niri_ = self.to_niri.clone();
let redraw = move || {
if let Some(output) = weak.upgrade() {
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) {
warn!("error sending Redraw to niri: {err:?}");
}
if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
warn!("error sending Redraw to niri: {err:?}");
}
};
let mode = output.current_mode().unwrap();
let size = mode.size;
let refresh = mode.refresh;
let redraw_ = redraw.clone();
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));
let dmabufs = Rc::new(RefCell::new(HashMap::new()));
let refresh = Rc::new(Cell::new(refresh));
let pending_size = Size::from((size.w as u32, size.h as u32));
let state = Rc::new(RefCell::new(CastState::ResizePending { pending_size }));
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 +259,12 @@ impl PipeWire {
})
.param_changed({
let min_time_between_frames = min_time_between_frames.clone();
move |stream, id, _data, pod| {
let stop_cast = stop_cast.clone();
let state = state.clone();
let gbm = gbm.clone();
let formats = formats.clone();
let refresh = refresh.clone();
move |stream, (), id, pod| {
let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed");
@@ -198,7 +288,28 @@ impl PipeWire {
let mut format = VideoInfoRaw::new();
format.parse(pod).unwrap();
trace!("pw stream: got format = {format:?}");
debug!("pw stream: got format = {format:?}");
let format_size = Size::from((format.size().width, format.size().height));
let mut state = state.borrow_mut();
if format_size != state.expected_format_size() {
if !matches!(&*state, CastState::ResizePending { .. }) {
warn!("pw stream: wrong size, but we're not resizing");
stop_cast();
return;
}
debug!("pw stream: wrong size, waiting");
return;
}
let format_has_alpha = format.format() == VideoFormat::BGRA;
let fourcc = if format_has_alpha {
Fourcc::Argb8888
} else {
Fourcc::Xrgb8888
};
let max_frame_rate = format.max_framerate();
// Subtract 0.5 ms to improve edge cases when equal to refresh rate.
@@ -207,9 +318,161 @@ impl PipeWire {
) - Duration::from_micros(500);
min_time_between_frames.set(min_frame_time);
const BPP: u32 = 4;
let stride = format.size().width * BPP;
let size = stride * format.size().height;
let object = pod.as_object().unwrap();
let Some(prop_modifier) =
object.find_prop(spa::utils::Id(FormatProperties::VideoModifier.0))
else {
warn!("pw stream: modifier prop missing");
stop_cast();
return;
};
if prop_modifier.flags().contains(PodPropFlags::DONT_FIXATE) {
debug!("pw stream: fixating the modifier");
let pod_modifier = prop_modifier.value();
let Ok((_, modifiers)) = PodDeserializer::deserialize_from::<Choice<i64>>(
pod_modifier.as_bytes(),
) else {
warn!("pw stream: wrong modifier property type");
stop_cast();
return;
};
let ChoiceEnum::Enum { alternatives, .. } = modifiers.1 else {
warn!("pw stream: wrong modifier choice type");
stop_cast();
return;
};
let (modifier, plane_count) = match find_preferred_modifier(
&gbm,
format_size,
fourcc,
alternatives,
) {
Ok(x) => x,
Err(err) => {
warn!("pw stream: couldn't find preferred modifier: {err:?}");
stop_cast();
return;
}
};
debug!(
"pw stream: allocation successful \
(modifier={modifier:?}, plane_count={plane_count}), \
moving to confirmation pending"
);
*state = CastState::ConfirmationPending {
size: format_size,
alpha: format_has_alpha,
modifier,
plane_count: plane_count as i32,
};
let fixated_format = FormatSet::from_iter([Format {
code: fourcc,
modifier,
}]);
let mut b1 = Vec::new();
let mut b2 = Vec::new();
let o1 = make_video_params(
&fixated_format,
format_size,
refresh.get(),
format_has_alpha,
);
let pod1 = make_pod(&mut b1, o1);
let o2 = make_video_params(
&formats,
format_size,
refresh.get(),
format_has_alpha,
);
let mut params = [pod1, make_pod(&mut b2, o2)];
if let Err(err) = stream.update_params(&mut params) {
warn!("error updating stream params: {err:?}");
stop_cast();
}
return;
}
// Verify that alpha and modifier didn't change.
let plane_count = match &*state {
CastState::ConfirmationPending {
size,
alpha,
modifier,
plane_count,
}
| CastState::Ready {
size,
alpha,
modifier,
plane_count,
} if *alpha == format_has_alpha
&& *modifier == Modifier::from(format.modifier()) =>
{
let size = *size;
let alpha = *alpha;
let modifier = *modifier;
let plane_count = *plane_count;
debug!("pw stream: moving to ready state");
*state = CastState::Ready {
size,
alpha,
modifier,
plane_count,
};
plane_count
}
_ => {
// We're negotiating a single modifier, or alpha or modifier changed,
// so we need to do a test allocation.
let (modifier, plane_count) = match find_preferred_modifier(
&gbm,
format_size,
fourcc,
vec![format.modifier() as i64],
) {
Ok(x) => x,
Err(err) => {
warn!("pw stream: test allocation failed: {err:?}");
stop_cast();
return;
}
};
debug!(
"pw stream: allocation successful \
(modifier={modifier:?}, plane_count={plane_count}), \
moving to ready"
);
*state = CastState::Ready {
size: format_size,
alpha: format_has_alpha,
modifier,
plane_count: plane_count as i32,
};
plane_count as i32
}
};
// const BPP: u32 = 4;
// let stride = format.size().width * BPP;
// let size = stride * format.size().height;
let o1 = pod::object!(
SpaTypes::ObjectParamBuffers,
@@ -225,10 +488,10 @@ impl PipeWire {
}
))),
),
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(1)),
Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(plane_count)),
// Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
// Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
// Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
Property::new(
SPA_PARAM_BUFFERS_dataType,
pod::Value::Choice(ChoiceValue::Int(Choice(
@@ -256,60 +519,83 @@ 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();
if let Err(err) = stream.update_params(&mut params) {
warn!("error updating stream params: {err:?}");
stop_cast();
}
}
})
.add_buffer({
let dmabufs = dmabufs.clone();
let stop_cast = stop_cast.clone();
move |buffer| {
trace!("pw stream: add_buffer");
let state = state.clone();
move |stream, (), buffer| {
let (size, alpha, modifier) = if let CastState::Ready {
size,
alpha,
modifier,
..
} = &*state.borrow()
{
(*size, *alpha, *modifier)
} else {
trace!("pw stream: add buffer, but not ready yet");
return;
};
trace!(
"pw stream: add_buffer, size={size:?}, alpha={alpha}, \
modifier={modifier:?}"
);
unsafe {
let spa_buffer = (*buffer).buffer;
let spa_data = (*spa_buffer).datas;
assert!((*spa_buffer).n_datas > 0);
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
let bo = match gbm.create_buffer_object::<()>(
size.w as u32,
size.h as u32,
Fourcc::Xrgb8888,
GbmBufferFlags::RENDERING | GbmBufferFlags::LINEAR,
) {
Ok(bo) => bo,
Err(err) => {
warn!("error creating GBM buffer object: {err:?}");
stop_cast();
return;
}
let fourcc = if alpha {
Fourcc::Argb8888
} else {
Fourcc::Xrgb8888
};
let dmabuf = match bo.export() {
let dmabuf = match allocate_dmabuf(&gbm, size, fourcc, modifier) {
Ok(dmabuf) => dmabuf,
Err(err) => {
warn!("error exporting GBM buffer object as dmabuf: {err:?}");
warn!("error allocating dmabuf: {err:?}");
stop_cast();
return;
}
};
let fd = dmabuf.handles().next().unwrap().as_raw_fd();
let plane_count = dmabuf.num_planes();
assert_eq!((*spa_buffer).n_datas as usize, plane_count);
(*spa_data).type_ = DataType::DmaBuf.as_raw();
(*spa_data).maxsize = dmabuf.strides().next().unwrap() * size.h as u32;
(*spa_data).fd = fd as i64;
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
for (i, fd) in dmabuf.handles().enumerate() {
let spa_data = (*spa_buffer).datas.add(i);
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
(*spa_data).type_ = DataType::DmaBuf.as_raw();
(*spa_data).maxsize = 1;
(*spa_data).fd = fd.as_raw_fd() as i64;
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
}
let fd = (*(*spa_buffer).datas).fd;
assert!(dmabufs.borrow_mut().insert(fd, dmabuf).is_none());
}
// During size re-negotiation, the stream sometimes just keeps running, in
// which case we may need to force a redraw once we got a newly sized buffer.
if dmabufs.borrow().len() == 1 && stream.state() == StreamState::Streaming {
redraw_();
}
}
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: remove_buffer");
unsafe {
@@ -317,7 +603,7 @@ impl PipeWire {
let spa_data = (*spa_buffer).datas;
assert!((*spa_buffer).n_datas > 0);
let fd = (*spa_data).fd as i32;
let fd = (*spa_data).fd;
dmabufs.borrow_mut().remove(&fd);
}
}
@@ -325,55 +611,16 @@ impl PipeWire {
.register()
.unwrap();
let object = pod::object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
pod::property!(FormatProperties::MediaType, Id, MediaType::Video),
pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
pod::property!(FormatProperties::VideoFormat, Id, VideoFormat::BGRx),
Property {
key: FormatProperties::VideoModifier.as_raw(),
value: pod::Value::Long(u64::from(Modifier::Invalid) as i64),
flags: PropertyFlags::MANDATORY,
},
pod::property!(
FormatProperties::VideoSize,
Rectangle,
Rectangle {
width: size.w as u32,
height: size.h as u32,
}
),
pod::property!(
FormatProperties::VideoFramerate,
Fraction,
Fraction { num: 0, denom: 1 }
),
pod::property!(
FormatProperties::VideoMaxFramerate,
Choice,
Range,
Fraction,
Fraction {
num: refresh as u32,
denom: 1000
},
Fraction { num: 1, denom: 1 },
Fraction {
num: refresh as u32,
denom: 1000
}
),
);
trace!("starting pw stream with size={pending_size:?}, refresh={refresh:?}");
let mut buffer = vec![];
let mut params = [make_pod(&mut buffer, object)];
let params;
make_params!(params, &formats, pending_size, refresh.get(), alpha);
stream
.connect(
Direction::Output,
None,
StreamFlags::DRIVER | StreamFlags::ALLOC_BUFFERS,
&mut params,
params,
)
.context("error connecting stream")?;
@@ -382,7 +629,11 @@ impl PipeWire {
stream,
_listener: listener,
is_active,
output,
target,
formats,
state,
refresh,
offer_alpha: alpha,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
@@ -392,7 +643,304 @@ impl PipeWire {
}
}
impl Cast {
pub fn ensure_size(&self, size: Size<i32, Physical>) -> anyhow::Result<CastSizeChange> {
let new_size = Size::from((size.w as u32, size.h as u32));
let mut state = self.state.borrow_mut();
if matches!(&*state, CastState::Ready { size, .. } if *size == new_size) {
return Ok(CastSizeChange::Ready);
}
if state.pending_size() == Some(new_size) {
debug!("stream size still hasn't changed, skipping frame");
return Ok(CastSizeChange::Pending);
}
let _span = tracy_client::span!("Cast::ensure_size");
debug!("cast size changed, updating stream size");
*state = CastState::ResizePending {
pending_size: new_size,
};
let params;
make_params!(
params,
&self.formats,
new_size,
self.refresh.get(),
self.offer_alpha
);
self.stream
.update_params(params)
.context("error updating stream params")?;
Ok(CastSizeChange::Pending)
}
pub fn set_refresh(&mut self, refresh: u32) -> anyhow::Result<()> {
if self.refresh.get() == refresh {
return Ok(());
}
let _span = tracy_client::span!("Cast::set_refresh");
debug!("cast FPS changed, updating stream FPS");
self.refresh.set(refresh);
let size = self.state.borrow().expected_format_size();
let params;
make_params!(params, &self.formats, size, refresh, self.offer_alpha);
self.stream
.update_params(params)
.context("error updating stream params")?;
Ok(())
}
pub fn should_skip_frame(&self, target_frame_time: Duration) -> bool {
let last = self.last_frame_time;
let min = self.min_time_between_frames.get();
if last.is_zero() {
trace!(?target_frame_time, ?last, "last is zero, recording");
return false;
}
if target_frame_time < last {
// Record frame with a warning; in case it was an overflow this will fix it.
warn!(
?target_frame_time,
?last,
"target frame time is below last, did it overflow or did we mispredict?"
);
return false;
}
let diff = target_frame_time - last;
if diff < min {
trace!(
?target_frame_time,
?last,
"skipping frame because it is too soon: diff={diff:?} < min={min:?}",
);
return true;
}
false
}
pub fn dequeue_buffer_and_render(
&mut self,
renderer: &mut GlesRenderer,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
size: Size<i32, Physical>,
scale: Scale<f64>,
) -> bool {
let mut buffer = match self.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
warn!("no available buffer in pw stream, skipping frame");
return false;
}
};
let fd = buffer.datas_mut()[0].as_raw().fd;
let dmabuf = &self.dmabufs.borrow()[&fd];
if let Err(err) = render_to_dmabuf(
renderer,
dmabuf.clone(),
size,
scale,
Transform::Normal,
elements,
) {
warn!("error rendering to dmabuf: {err:?}");
return false;
}
for (data, (stride, offset)) in
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
{
let chunk = data.chunk_mut();
*chunk.size_mut() = 1;
*chunk.stride_mut() = stride as i32;
*chunk.offset_mut() = offset;
trace!(
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
data.as_raw().fd
);
}
true
}
}
impl CastState {
fn pending_size(&self) -> Option<Size<u32, Physical>> {
match self {
CastState::ResizePending { pending_size } => Some(*pending_size),
CastState::ConfirmationPending { size, .. } => Some(*size),
CastState::Ready { .. } => None,
}
}
fn expected_format_size(&self) -> Size<u32, Physical> {
match self {
CastState::ResizePending { pending_size } => *pending_size,
CastState::ConfirmationPending { size, .. } => *size,
CastState::Ready { size, .. } => *size,
}
}
}
fn make_video_params(
formats: &FormatSet,
size: Size<u32, Physical>,
refresh: u32,
alpha: bool,
) -> pod::Object {
let format = if alpha {
VideoFormat::BGRA
} else {
VideoFormat::BGRx
};
let fourcc = if alpha {
Fourcc::Argb8888
} else {
Fourcc::Xrgb8888
};
let formats: Vec<_> = formats
.iter()
.filter_map(|f| (f.code == fourcc).then_some(u64::from(f.modifier) as i64))
.collect();
trace!("offering: {formats:?}");
let dont_fixate = if formats.len() > 1 {
PropertyFlags::DONT_FIXATE
} else {
PropertyFlags::empty()
};
pod::object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
pod::property!(FormatProperties::MediaType, Id, MediaType::Video),
pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
pod::property!(FormatProperties::VideoFormat, Id, format),
Property {
key: FormatProperties::VideoModifier.as_raw(),
flags: PropertyFlags::MANDATORY | dont_fixate,
value: pod::Value::Choice(ChoiceValue::Long(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Enum {
default: formats[0],
alternatives: formats,
}
)))
},
pod::property!(
FormatProperties::VideoSize,
Rectangle,
Rectangle {
width: size.w,
height: size.h,
}
),
pod::property!(
FormatProperties::VideoFramerate,
Fraction,
Fraction { num: 0, denom: 1 }
),
pod::property!(
FormatProperties::VideoMaxFramerate,
Choice,
Range,
Fraction,
Fraction {
num: refresh,
denom: 1000
},
Fraction { num: 1, denom: 1 },
Fraction {
num: refresh,
denom: 1000
}
),
)
}
fn make_pod(buffer: &mut Vec<u8>, object: pod::Object) -> &Pod {
PodSerializer::serialize(Cursor::new(&mut *buffer), &pod::Value::Object(object)).unwrap();
Pod::from_bytes(buffer).unwrap()
}
fn find_preferred_modifier(
gbm: &GbmDevice<DrmDeviceFd>,
size: Size<u32, Physical>,
fourcc: Fourcc,
modifiers: Vec<i64>,
) -> anyhow::Result<(Modifier, usize)> {
debug!("find_preferred_modifier: size={size:?}, fourcc={fourcc}, modifiers={modifiers:?}");
let (buffer, modifier) = allocate_buffer(gbm, size, fourcc, &modifiers)?;
let dmabuf = buffer
.export()
.context("error exporting GBM buffer object as dmabuf")?;
let plane_count = dmabuf.num_planes();
// FIXME: Ideally this also needs to try binding the dmabuf for rendering.
Ok((modifier, plane_count))
}
fn allocate_buffer(
gbm: &GbmDevice<DrmDeviceFd>,
size: Size<u32, Physical>,
fourcc: Fourcc,
modifiers: &[i64],
) -> anyhow::Result<(GbmBuffer, Modifier)> {
let (w, h) = (size.w, size.h);
let flags = GbmBufferFlags::RENDERING;
if modifiers.len() == 1 && Modifier::from(modifiers[0] as u64) == Modifier::Invalid {
let bo = gbm
.create_buffer_object::<()>(w, h, fourcc, flags)
.context("error creating GBM buffer object")?;
let buffer = GbmBuffer::from_bo(bo, true);
Ok((buffer, Modifier::Invalid))
} else {
let modifiers = modifiers
.iter()
.map(|m| Modifier::from(*m as u64))
.filter(|m| *m != Modifier::Invalid);
let bo = gbm
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
.context("error creating GBM buffer object")?;
let modifier = bo.modifier().unwrap();
let buffer = GbmBuffer::from_bo(bo, false);
Ok((buffer, modifier))
}
}
fn allocate_dmabuf(
gbm: &GbmDevice<DrmDeviceFd>,
size: Size<u32, Physical>,
fourcc: Fourcc,
modifier: Modifier,
) -> anyhow::Result<Dmabuf> {
let (buffer, _modifier) = allocate_buffer(gbm, size, fourcc, &[u64::from(modifier) as i64])?;
let dmabuf = buffer
.export()
.context("error exporting GBM buffer object as dmabuf")?;
Ok(dmabuf)
}
-168
View File
@@ -1,168 +0,0 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use 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>);
impl Element for PrimaryGpuTextureRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
for PrimaryGpuTextureRenderElement
{
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render, 'alloc>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render, 'alloc>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+301
View File
@@ -0,0 +1,301 @@
use std::collections::HashMap;
use glam::{Mat3, Vec2};
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::renderer::NiriRenderer;
use super::shader_element::ShaderRenderElement;
use super::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a wide variety of borders and border parts.
///
/// This includes:
/// * sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
/// * corner rounding.
/// * as a background rectangle and as parts of a border line.
#[derive(Debug, Clone)]
pub struct BorderRenderElement {
inner: ShaderRenderElement,
params: Parameters,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Parameters {
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
}
impl BorderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
) -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
let mut rv = Self {
inner,
params: Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
},
};
rv.update_inner();
rv
}
pub fn empty() -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
Self {
inner,
params: Parameters {
size: Default::default(),
gradient_area: Default::default(),
gradient_format: GradientInterpolation::default(),
color_from: Default::default(),
color_to: Default::default(),
angle: 0.,
geometry: Default::default(),
border_width: 0.,
corner_radius: Default::default(),
scale: 1.,
},
}
}
pub fn damage_all(&mut self) {
self.inner.damage_all();
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
) {
let params = Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
};
if self.params == params {
return;
}
self.params = params;
self.update_inner();
}
fn update_inner(&mut self) {
let Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
} = self.params;
let grad_offset = geometry.loc - gradient_area.loc;
let grad_offset = Vec2::new(grad_offset.x as f32, grad_offset.y as f32);
let grad_dir = Vec2::from_angle(angle);
let (w, h) = (gradient_area.size.w as f32, gradient_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 area_size = Vec2::new(size.w as f32, size.h as f32);
let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32);
let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32);
let input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
let colorspace = match gradient_format.color_space {
GradientColorSpace::Srgb => 0.,
GradientColorSpace::SrgbLinear => 1.,
GradientColorSpace::Oklab => 2.,
GradientColorSpace::Oklch => 3.,
};
let hue_interpolation = match gradient_format.hue_interpolation {
HueInterpolation::Shorter => 0.,
HueInterpolation::Longer => 1.,
HueInterpolation::Increasing => 2.,
HueInterpolation::Decreasing => 3.,
};
self.inner.update(
size,
None,
scale,
vec![
Uniform::new("colorspace", colorspace),
Uniform::new("hue_interpolation", hue_interpolation),
Uniform::new("color_from", color_from.to_array_unpremul()),
Uniform::new("color_to", color_to.to_array_unpremul()),
Uniform::new("grad_offset", grad_offset.to_array()),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
mat3_uniform("input_to_geo", input_to_geo),
Uniform::new("geo_size", geo_size.to_array()),
Uniform::new("outer_radius", <[f32; 4]>::from(corner_radius)),
Uniform::new("border_width", border_width),
],
HashMap::new(),
);
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Border)
.is_some()
}
}
impl Default for BorderRenderElement {
fn default() -> Self {
Self::empty()
}
}
impl Element for BorderRenderElement {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.inner.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.inner.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for BorderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
+302
View File
@@ -0,0 +1,302 @@
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::damage::ExtraDamage;
use super::renderer::{AsGlesFrame as _, NiriRenderer};
use super::shaders::{mat3_uniform, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
#[derive(Debug)]
pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
inner: WaylandSurfaceRenderElement<R>,
program: GlesTexProgram,
corner_radius: CornerRadius,
geometry: Rectangle<f64, Logical>,
input_to_geo: Mat3,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
}
#[derive(Debug, Default, Clone)]
pub struct RoundedCornerDamage {
damage: ExtraDamage,
corner_radius: CornerRadius,
}
impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn new(
elem: WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<f64, Logical>,
program: GlesTexProgram,
corner_radius: CornerRadius,
) -> Self {
let elem_geo = elem.geometry(scale);
let elem_geo_loc = Vec2::new(elem_geo.loc.x as f32, elem_geo.loc.y as f32);
let elem_geo_size = Vec2::new(elem_geo.size.w as f32, elem_geo.size.h as f32);
let geo = geometry.to_physical_precise_round(scale);
let geo_loc = Vec2::new(geo.loc.x, geo.loc.y);
let geo_size = Vec2::new(geo.size.w, geo.size.h);
let buf_size = elem.buffer_size();
let buf_size = Vec2::new(buf_size.w as f32, buf_size.h as f32);
let view = elem.view();
let src_loc = Vec2::new(view.src.loc.x as f32, view.src.loc.y as f32);
let src_size = Vec2::new(view.src.size.w as f32, view.src.size.h as f32);
let transform = elem.transform();
// HACK: ??? for some reason flipped ones are fine.
let transform = match transform {
Transform::_90 => Transform::_270,
Transform::_270 => Transform::_90,
x => x,
};
let transform_matrix = Mat3::from_translation(Vec2::new(0.5, 0.5))
* Mat3::from_cols_array(transform.matrix().as_ref())
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
// FIXME: y_inverted
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
// Apply viewporter src.
* Mat3::from_scale(buf_size / src_size)
* Mat3::from_translation(-src_loc / buf_size);
Self {
inner: elem,
program,
corner_radius,
geometry,
input_to_geo,
scale: scale.x as f32,
}
}
pub fn shader(renderer: &mut R) -> Option<&GlesTexProgram> {
Shaders::get(renderer).clipped_surface.as_ref()
}
pub fn will_clip(
elem: &WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> bool {
let elem_geo = elem.geometry(scale);
let geo = geometry.to_physical_precise_round(scale);
if corner_radius == CornerRadius::default() {
!geo.contains_rect(elem_geo)
} else {
let corners = Self::rounded_corners(geometry, corner_radius);
let corners = corners
.into_iter()
.map(|rect| rect.to_physical_precise_up(scale));
let geo = Rectangle::subtract_rects_many([geo], corners);
!Rectangle::subtract_rects_many([elem_geo], geo).is_empty()
}
}
fn rounded_corners(
geo: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> [Rectangle<f64, Logical>; 4] {
let top_left = corner_radius.top_left as f64;
let top_right = corner_radius.top_right as f64;
let bottom_right = corner_radius.bottom_right as f64;
let bottom_left = corner_radius.bottom_left as f64;
[
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
Rectangle::from_loc_and_size(
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
(top_right, top_right),
),
Rectangle::from_loc_and_size(
(
geo.loc.x + geo.size.w - bottom_right,
geo.loc.y + geo.size.h - bottom_right,
),
(bottom_right, bottom_right),
),
Rectangle::from_loc_and_size(
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
(bottom_left, bottom_left),
),
]
}
}
impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
// FIXME: radius changes need to cause damage.
let damage = self.inner.damage_since(scale, commit);
// Intersect with geometry, since we're clipping by it.
let mut geo = self.geometry.to_physical_precise_round(scale);
geo.loc -= self.geometry(scale).loc;
damage
.into_iter()
.filter_map(|rect| rect.intersection(geo))
.collect()
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
let regions = self.inner.opaque_regions(scale);
// Intersect with geometry, since we're clipping by it.
let mut geo = self.geometry.to_physical_precise_round(scale);
geo.loc -= self.geometry(scale).loc;
let regions = regions
.into_iter()
.filter_map(|rect| rect.intersection(geo));
// Subtract the rounded corners.
if self.corner_radius == CornerRadius::default() {
regions.collect()
} else {
let corners = Self::rounded_corners(self.geometry, self.corner_radius);
let elem_loc = self.geometry(scale).loc;
let corners = corners.into_iter().map(|rect| {
let mut rect = rect.to_physical_precise_up(scale);
rect.loc -= elem_loc;
rect
});
OpaqueRegions::from_slice(&Rectangle::subtract_rects_many(regions, corners))
}
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
frame.override_default_tex_program(
self.program.clone(),
vec![
Uniform::new("niri_scale", self.scale),
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.clear_tex_program_override();
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 ClippedSurfaceRenderElement<TtyRenderer<'render>>
{
fn draw(
&self,
frame: &mut TtyFrame<'render, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
frame.as_gles_frame().override_default_tex_program(
self.program.clone(),
vec![
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.as_gles_frame().clear_tex_program_override();
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
}
}
impl RoundedCornerDamage {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
self.damage.set_size(size);
}
pub fn set_corner_radius(&mut self, corner_radius: CornerRadius) {
if self.corner_radius == corner_radius {
return;
}
// FIXME: make the damage granular.
self.corner_radius = corner_radius;
self.damage.damage_all();
}
pub fn element(&self) -> ExtraDamage {
self.damage.clone()
}
}
+76
View File
@@ -0,0 +1,76 @@
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::Renderer;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
#[derive(Debug, Clone)]
pub struct ExtraDamage {
id: Id,
commit: CommitCounter,
geometry: Rectangle<f64, Logical>,
}
impl ExtraDamage {
pub fn new() -> Self {
Self {
id: Id::new(),
commit: Default::default(),
geometry: Default::default(),
}
}
pub fn set_size(&mut self, size: Size<f64, Logical>) {
if self.geometry.size == size {
return;
}
self.geometry.size = size;
self.commit.increment();
}
pub fn damage_all(&mut self) {
self.commit.increment();
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.geometry.loc = location;
self
}
}
impl Default for ExtraDamage {
fn default() -> Self {
Self::new()
}
}
impl Element for ExtraDamage {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_up(scale)
}
}
impl<R: Renderer> RenderElement<R> for ExtraDamage {
fn draw(
&self,
_frame: &mut <R as Renderer>::Frame<'_>,
_src: Rectangle<f64, Buffer>,
_dst: Rectangle<i32, Physical>,
_damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), R::Error> {
Ok(())
}
}
+81
View File
@@ -0,0 +1,81 @@
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::Scale;
use super::renderer::NiriRenderer;
use crate::niri::OutputRenderElements;
pub fn draw_opaque_regions<R: NiriRenderer>(
elements: &mut Vec<OutputRenderElements<R>>,
scale: Scale<f64>,
) {
let _span = tracy_client::span!("draw_opaque_regions");
let mut i = 0;
while i < elements.len() {
let elem = &elements[i];
i += 1;
// HACK
if format!("{elem:?}").contains("ExtraDamage") {
continue;
}
let geo = elem.geometry(scale);
let mut opaque = elem.opaque_regions(scale).to_vec();
for rect in &mut opaque {
rect.loc += geo.loc;
}
let semitransparent = geo.subtract_rects(opaque.iter().copied());
for rect in opaque {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
CommitCounter::default(),
[0., 0., 0.2, 0.2],
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
i += 1;
}
for rect in semitransparent {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
i += 1;
}
}
}
pub fn draw_damage<R: NiriRenderer>(
damage_tracker: &mut OutputDamageTracker,
elements: &mut Vec<OutputRenderElements<R>>,
) {
let _span = tracy_client::span!("draw_damage");
let Ok((Some(damage), _)) = damage_tracker.damage_output(1, elements) else {
return;
};
for rect in damage {
let color = SolidColorRenderElement::new(
Id::new(),
*rect,
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Kind::Unspecified,
);
elements.insert(0, OutputRenderElements::SolidColor(color));
}
}
+63
View File
@@ -0,0 +1,63 @@
use std::sync::Arc;
use smithay::backend::allocator::format::get_bpp;
use smithay::backend::allocator::Fourcc;
use smithay::utils::{Buffer, Logical, Scale, Size, Transform};
#[derive(Clone)]
pub struct MemoryBuffer {
data: Arc<[u8]>,
format: Fourcc,
size: Size<i32, Buffer>,
scale: Scale<f64>,
transform: Transform,
}
impl MemoryBuffer {
pub fn new(
data: impl Into<Arc<[u8]>>,
format: Fourcc,
size: impl Into<Size<i32, Buffer>>,
scale: impl Into<Scale<f64>>,
transform: Transform,
) -> Self {
let data = data.into();
let size = size.into();
let stride =
size.w * (get_bpp(format).expect("Format with unknown bits per pixel") / 8) as i32;
assert!(data.len() >= (stride * size.h) as usize);
Self {
data,
format,
size,
scale: scale.into(),
transform,
}
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn format(&self) -> Fourcc {
self.format
}
pub fn size(&self) -> Size<i32, Buffer> {
self.size
}
pub fn scale(&self) -> Scale<f64> {
self.scale
}
pub fn transform(&self) -> Transform {
self.transform
}
pub fn logical_size(&self) -> Size<f64, Logical> {
self.size.to_f64().to_logical(self.scale, self.transform)
}
}
+321
View File
@@ -0,0 +1,321 @@
use std::ptr;
use anyhow::{ensure, Context};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::{Buffer, Fourcc};
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::{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 solid_color::{SolidColorBuffer, SolidColorRenderElement};
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use self::texture::{TextureBuffer, TextureRenderElement};
pub mod border;
pub mod clipped_surface;
pub mod damage;
pub mod debug;
pub mod memory;
pub mod offscreen;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
pub mod resize;
pub mod resources;
pub mod shader_element;
pub mod shaders;
pub mod snapshot;
pub mod solid_color;
pub mod surface;
pub mod texture;
/// 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<f64, Logical>,
pub src: Option<Rectangle<f64, Logical>>,
pub dst: Option<Size<i32, Logical>>,
}
/// Render elements split into normal and popup.
#[derive(Debug)]
pub struct SplitElements<E> {
pub normal: Vec<E>,
pub popups: Vec<E>,
}
pub trait ToRenderElement {
type RenderElement;
fn to_render_element(
&self,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement;
}
impl RenderTarget {
pub fn should_block_out(self, block_out_from: Option<BlockOutFrom>) -> bool {
match block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => self == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => self != RenderTarget::Output,
}
}
}
impl<E> Default for SplitElements<E> {
fn default() -> Self {
Self {
normal: Vec::new(),
popups: Vec::new(),
}
}
}
impl<E> IntoIterator for SplitElements<E> {
type Item = E;
type IntoIter = std::iter::Chain<std::vec::IntoIter<E>, std::vec::IntoIter<E>>;
fn into_iter(self) -> Self::IntoIter {
self.popups.into_iter().chain(self.normal)
}
}
impl<E> SplitElements<E> {
pub fn iter(&self) -> std::iter::Chain<std::slice::Iter<E>, std::slice::Iter<E>> {
self.popups.iter().chain(&self.normal)
}
pub fn into_vec(self) -> Vec<E> {
let Self { normal, mut popups } = self;
popups.extend(normal);
popups
}
}
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
type RenderElement = PrimaryGpuTextureRenderElement;
fn to_render_element(
&self,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
let elem = TextureRenderElement::from_texture_buffer(
self.buffer.clone(),
location + self.location,
alpha,
self.src,
self.dst.map(|dst| dst.to_f64()),
kind,
);
PrimaryGpuTextureRenderElement(elem)
}
}
impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
type RenderElement = SolidColorRenderElement;
fn to_render_element(
&self,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
SolidColorRenderElement::from_buffer(&self.buffer, location + self.location, 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())
}
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
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!();
ensure!(
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
"invalid buffer size"
);
renderer.bind(dmabuf).context("error binding texture")?;
render_elements(renderer, size, scale, transform, elements)
}
pub fn render_to_shm(
renderer: &mut GlesRenderer,
buffer: &WlBuffer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
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::Xrgb8888
&& buffer_data.width == size.w
&& buffer_data.height == size.h
&& buffer_data.stride == size.w * 4
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize,
"invalid buffer format or size"
);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Xrgb8888, elements)?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
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")
}
+251
View File
@@ -0,0 +1,251 @@
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::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use super::texture::{TextureBuffer, TextureRenderElement};
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 as f64,
Transform::Normal,
Vec::new(),
);
let element = TextureRenderElement::from_texture_buffer(
buffer,
geo.loc.to_f64().to_logical(scale as f64),
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>) -> OpaqueRegions<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>],
opaque_regions: &[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,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
}
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>],
opaque_regions: &[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,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
}
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)
}
}
}
+99
View File
@@ -0,0 +1,99 @@
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, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use super::texture::TextureRenderElement;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug, Clone)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
impl Element for PrimaryGpuTextureRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
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 PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
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
}
}
+150
View File
@@ -0,0 +1,150 @@
// 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>) -> smithay::backend::renderer::utils::OpaqueRegions<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>],
opaque_regions: &[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, opaque_regions)
})+
}
}
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>],
opaque_regions: &[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, opaque_regions)
})+
}
}
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 + Send + '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 + Send + '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()
}
}
+198
View File
@@ -0,0 +1,198 @@
use std::collections::HashMap;
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
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, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::renderer::{AsGlesFrame, NiriRenderer};
use super::shader_element::ShaderRenderElement;
use super::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
#[derive(Debug)]
pub struct ResizeRenderElement(ShaderRenderElement);
impl ResizeRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
area: Rectangle<f64, Logical>,
scale: Scale<f64>,
texture_prev: (GlesTexture, Rectangle<i32, Physical>),
size_prev: Size<f64, Logical>,
texture_next: (GlesTexture, Rectangle<i32, Physical>),
size_next: Size<f64, Logical>,
progress: f32,
clamped_progress: f32,
corner_radius: CornerRadius,
clip_to_geometry: bool,
result_alpha: f32,
) -> Self {
let curr_geo = area;
let (texture_prev, tex_prev_geo) = texture_prev;
let (texture_next, tex_next_geo) = texture_next;
let scale_prev = area.size / size_prev;
let scale_next = area.size / size_next;
// Compute the area necessary to fit a crossfade.
let tex_prev_geo_scaled = tex_prev_geo.to_f64().upscale(scale_prev);
let tex_next_geo_scaled = tex_next_geo.to_f64().upscale(scale_next);
let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled).to_i32_up();
let area = Rectangle::from_loc_and_size(
area.loc + combined_geo.loc.to_logical(scale),
combined_geo.size.to_logical(scale),
);
// Convert Smithay types into glam types.
let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32);
let area_size = Vec2::new(area.size.w as f32, area.size.h as f32);
let curr_geo_loc = Vec2::new(curr_geo.loc.x as f32, curr_geo.loc.y as f32);
let curr_geo_size = Vec2::new(curr_geo.size.w as f32, curr_geo.size.h as f32);
let tex_prev_geo_loc = Vec2::new(tex_prev_geo.loc.x as f32, tex_prev_geo.loc.y as f32);
let tex_prev_geo_size = Vec2::new(tex_prev_geo.size.w as f32, tex_prev_geo.size.h as f32);
let tex_next_geo_loc = Vec2::new(tex_next_geo.loc.x as f32, tex_next_geo.loc.y as f32);
let tex_next_geo_size = Vec2::new(tex_next_geo.size.w as f32, tex_next_geo.size.h as f32);
let size_prev = Vec2::new(size_prev.w as f32, size_prev.h as f32);
let size_next = Vec2::new(size_next.w as f32, size_next.h as f32);
let scale = Vec2::new(scale.x as f32, scale.y as f32);
// Compute the transformation matrices.
let input_to_curr_geo = Mat3::from_scale(area_size / curr_geo_size)
* Mat3::from_translation((area_loc - curr_geo_loc) / area_size);
let curr_geo_to_prev_geo = Mat3::from_scale(curr_geo_size / size_prev);
let curr_geo_to_next_geo = Mat3::from_scale(curr_geo_size / size_next);
let geo_to_tex_prev = Mat3::from_translation(-tex_prev_geo_loc / tex_prev_geo_size)
* Mat3::from_scale(size_prev / tex_prev_geo_size * scale);
let geo_to_tex_next = Mat3::from_translation(-tex_next_geo_loc / tex_next_geo_size)
* Mat3::from_scale(size_next / tex_next_geo_size * scale);
let corner_radius = corner_radius.fit_to(curr_geo_size.x, curr_geo_size.y);
let clip_to_geometry = if clip_to_geometry { 1. } else { 0. };
// Create the shader.
Self(
ShaderRenderElement::new(
ProgramType::Resize,
area.size,
None,
scale.x,
result_alpha,
vec![
mat3_uniform("niri_input_to_curr_geo", input_to_curr_geo),
mat3_uniform("niri_curr_geo_to_prev_geo", curr_geo_to_prev_geo),
mat3_uniform("niri_curr_geo_to_next_geo", curr_geo_to_next_geo),
Uniform::new("niri_curr_geo_size", curr_geo_size.to_array()),
mat3_uniform("niri_geo_to_tex_prev", geo_to_tex_prev),
mat3_uniform("niri_geo_to_tex_next", geo_to_tex_next),
Uniform::new("niri_progress", progress),
Uniform::new("niri_clamped_progress", clamped_progress),
Uniform::new("niri_corner_radius", <[f32; 4]>::from(corner_radius)),
Uniform::new("niri_clip_to_geometry", clip_to_geometry),
],
HashMap::from([
(String::from("niri_tex_prev"), texture_prev),
(String::from("niri_tex_next"), texture_next),
]),
Kind::Unspecified,
)
.with_location(area.loc),
)
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Resize)
.is_some()
}
}
impl Element for ResizeRenderElement {
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>) -> OpaqueRegions<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 ResizeRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+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:?}");
}
}
}

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