Compare commits

...

321 Commits

Author SHA1 Message Date
Ivan Molodetskikh 012340c5f4 Freeze view when pointer or touch is grabbed 2024-10-28 21:18:26 +03:00
Ivan Molodetskikh 6ecbf2db8a Deny toplevel move from DnD grabs
Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
2024-10-28 21:12:58 +03:00
Ivan Molodetskikh c9be9056ef Update Smithay 2024-10-28 21:12:58 +03:00
Ivan Molodetskikh 0866990b7d wiki/Gestures: Add interactive move 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh f04befb567 wiki: Document insert-hint config 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh da3e5c4424 Implement touch interactive resize 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh 26ab4dfb87 Implement touch interactive move 2024-10-27 23:07:39 -07:00
Rasmus Eneman e887ee93a3 Implement interactive window move 2024-10-27 23:07:39 -07:00
Ivan Molodetskikh d640e85158 Require Clone for LayoutElement::Id
Now that we have MappedId, this could really be Copy. But it's quite a
big refactor, so for now just require Clone as I'll need it.
2024-10-27 23:07:39 -07:00
gmorer c8044a9b5d ShaderRenderElement use borrowed Uniforms to minimize copy (#756) 2024-10-24 07:42:19 +03:00
Ivan Molodetskikh 289ae3604d tty: Guard against output disappearing immediately after connection
Fixes https://github.com/YaLTeR/niri/issues/739
2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 55fb885256 Use new Smithay method for turning off DPMS 2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 73a531f8bc Update dependencies (wl_output.scale fix) 2024-10-20 20:18:56 +03:00
Ivan Molodetskikh 10f04fd19d layout: Update tile config in Column::add_tile_at() 2024-10-19 12:33:44 +03:00
Christian Meissl 79fd309d6c support binding actions to switches (#747)
* support spawn action on switch events

this adds a new config section named `switch-events`
that allows to bind `spawn` action to certain switch
toggles.

* Expand docs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-18 14:00:40 +00:00
Ivan Molodetskikh dd8b2be044 layout: Add missing active idx check before setting activate prev on removal 2024-10-18 10:06:09 +03:00
Ivan Molodetskikh 8d08782eba Set CLOEXEC on logind inhibit fd
Don't leak it to child processes.
2024-10-17 08:59:06 +03:00
Ivan Molodetskikh 8555f37dbf layout: Use remove_column_by_idx in remove_tile_by_idx 2024-10-17 08:59:06 +03:00
Ivan Molodetskikh 4b837f429c layout: Accept anim_config in remove_column_by_idx 2024-10-17 08:59:06 +03:00
chillinbythetree a480087618 Add scroll-button property for Touchpad, Mouse, Trackpoint, Trackball (#744) 2024-10-17 05:43:47 +00:00
tazjin 84655d3b26 Implement input configuration for trackballs (#743)
* niri-config: add trackball configuration struct

The available options are mostly the same as for mice. I've verified that each
option is applicable to trackballs in the libinput CLI.

* input: apply trackball config settings
2024-10-16 13:51:56 +00:00
Ivan Molodetskikh 40843cbda1 layout/monitor: Extract workspace_under() 2024-10-16 09:39:34 +03:00
Ivan Molodetskikh a13b9298c6 Draw the layout as inactive when layer-shell has focus 2024-10-15 11:11:57 +03:00
Christian Meissl 0c5e046820 input: apply output transform for tablet input (#737)
when mapping a tablet input to an output apply
the output transform just like we already do for
touch input.
2024-10-15 11:11:15 +03:00
Ivan Molodetskikh 907ebc4977 Add boxed_union proptest-derive feature
Our Op enum grew large enough to trigger a stack overflow in
proptest-derive's generated code. Thankfully, this feature works around
the problem.
2024-10-15 10:06:55 +03:00
sodiboo e4161be1bf flake: use nightly rust-analyzer and add rust-src component (#735)
this also improves the application of overlays to be more uniform; what
was previously done was just Wrong
2024-10-15 08:21:49 +03:00
Ivan Molodetskikh be7fbd418f layout: Return Tile + info upon removal 2024-10-14 18:08:44 +03:00
Ivan Molodetskikh 06ec9eecdb layout/tests: Use existing method 2024-10-14 17:39:55 +03:00
Ivan Molodetskikh 79eef5ee90 layout: Remove unnecessary vec lookup 2024-10-14 17:36:00 +03:00
Ivan Molodetskikh 29602ca995 layout: Extract Monitor::workspaces_with_render_positions() 2024-10-14 11:08:44 +03:00
Mark Karlinsky d7156df842 Add support for running as a dinit service (#728)
* Added dinit services

* Added dinit support to niri-session

* Replaced shutdown script for dinit with a single command execution

* Added dinit service files to Getting Started install tables

* Fix typo in resources/dinit/niri

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

* Fixed mistakes in wiki/Getting-Started.md

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

* niri-session does not start dinit anymore

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-13 12:26:16 +00:00
Ivan Molodetskikh 33b39913c7 layout: Fix expel animation of the smaller window in column 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh d5cbc35811 Implement ConsumeOrExpelWindow{Left,Right} by id 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh a038c5aaab layout/workspace: Add add_tile_to_column() 2024-10-12 09:58:03 +03:00
Ivan Molodetskikh c9c985c927 Support empty column in tile_offsets
Will be needed for the new inserting tile code.
2024-10-11 11:00:50 +03:00
Ivan Molodetskikh 859c0be0e5 layout: Add clarifying comment 2024-10-10 10:44:18 +03:00
Ivan Molodetskikh 810ea245f9 layout: Deduplicate default width resolution 2024-10-10 10:40:59 +03:00
Ivan Molodetskikh 58fc5f3b06 layout: Replace move_window_to_output with move_to_output 2024-10-10 10:28:55 +03:00
Ivan Molodetskikh 7d4e99b760 layout/workspace: Reduce code duplication in adding windows 2024-10-10 10:17:16 +03:00
Ivan Molodetskikh ab7d81aae0 layout: Reduce field visibility
The outside code isn't supposed to mess with the fields.
2024-10-10 09:24:20 +03:00
Winter e24723125f added power-on-monitors (#723) 2024-10-09 08:50:06 +00:00
Ivan Molodetskikh 03c603918d Document the new cursor hide settings 2024-10-06 22:09:19 -07:00
Ivan Molodetskikh 6fb60dacd2 Rework pointer inactivity hide as a timer
The previous way was prone to triggering late due to compositor idling
and therefore never calling the check function.
2024-10-06 22:09:19 -07:00
yzy-1 42a9daec9d Implement hide cursor on key press and on timeout 2024-10-06 22:09:19 -07:00
Ivan Molodetskikh 1ba2be3928 Show hidden pointer on mouse press
Feels like this should be the case.
2024-10-06 22:09:19 -07:00
sodiboo 66be000410 implement locked cursor position hints (#685)
* implement cursor position hints

* Remove redundant fully qualified path

* Find root surface

* Convert nesting to if-return

* Manually wrap error messages

* Remove error!() prints

* Add queue redraw

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-10-06 20:36:49 +03:00
sodiboo 5fc669c282 remove redundant pointer casts in shader code 2024-10-05 22:26:47 -07:00
sodiboo 9b78b15ba5 use CStr literals over calling CStr::from_bytes_with_nul 2024-10-05 22:26:47 -07:00
sodiboo b9fd0a405e use if let Some() over match with None => () 2024-10-05 22:26:47 -07:00
seth 1b44e0cd20 flake: add overlay output 2024-10-05 12:09:24 -07:00
seth b3d4d4eacc flake: use rust-overlay in dev shell
This allows `niri-visual-tests` to still be built and run in the dev
shell where it's necessary, as well as brings back the nightly `rustfmt`
used by the project

We can't use `fenix` again though as it doesn't wrap `ld` like nixpkgs
and rust-overlay do; without it, the way we link `dlopen()`'d libraries
breaks
2024-10-05 12:09:24 -07:00
seth a835bdc940 ci: nix build -> nix flake check
The (debug) package is already set as a check and will still be built
with this, but Nix will now also check other outputs automatically --
such as the dev shell
2024-10-05 12:09:24 -07:00
seth b258fd69d2 flake: improve packaging
Some highlights include:

- Removing some unnecessary dependencies of the package itself
- Allowing for overriding the package
- Adding Cargo feature toggles
- Installing all niri-related resources
- Avoiding `LD_LIBRARY_PATH` hacks
2024-10-05 12:09:24 -07:00
seth 3ab3e778ab flake: drop most external inputs
Previously, inputs like Crane and Fenix were used to only build the
`niri` package. This isn't really required, and can easily be replaced
by nixpkgs' `rustPlatform` -- which will also lead to less dependencies
being pulled into user's lockfiles
2024-10-05 12:09:24 -07:00
seth e6203313ce flake: format with nixfmt 2024-10-05 12:09:24 -07:00
seth 938061dd5e flake: use nixfmt 2024-10-05 12:09:24 -07:00
Ivan Molodetskikh 0cca7a2116 default-config: Add more comments to prefer-no-csd 2024-10-01 13:28:28 +03:00
Ivan Molodetskikh 39b46b3326 default-config: Add rounded corner window rule example 2024-10-01 13:28:16 +03:00
Ivan Molodetskikh 2aebd6bdbb default-config: Add comments to consume/expel binds 2024-10-01 13:20:38 +03:00
Ivan Molodetskikh b501a9b303 Upgrade dependencies 2024-09-30 15:27:36 +03:00
Ivan Molodetskikh 94e5408f46 Update Smithay 2024-09-30 15:24:50 +03:00
Christian Meissl eb190e3f94 handle role specific buffer offset 2024-09-30 05:04:58 -07:00
spazzylemons 80bb0d5876 Remove one unnecessary .clone() call and reorder another 2024-09-30 00:45:44 -07:00
Marwin Kreuzig c04ccafd0a fix focus_up_or_right 2024-09-28 05:18:22 -07:00
sodiboo 6ee5b5afa7 flake: update inputs and remove crane.inputs.nixpkgs override
the input was removed in https://github.com/ipetkov/crane/pull/692
2024-09-15 08:05:05 -07:00
Ivan Molodetskikh 6a48728ffb Bump version to 0.1.9 2024-09-14 11:55:52 +03:00
Ivan Molodetskikh 9cb89ff26c wiki: Update default hotkeys list 2024-09-14 10:17:27 +03:00
Ivan Molodetskikh 4e5f392c50 wiki: Document always-center-focused-column 2024-09-14 09:48:59 +03:00
Ivan Molodetskikh e35d9e760b default-config: Uncomment BracketLeft/BracketRight
These are fairly useful.
2024-09-13 21:51:56 +03:00
Ivan Molodetskikh 22fee7b003 Add NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY env
Useful for UWSM I guess.
2024-09-13 15:45:30 +03:00
Ivan Molodetskikh e95d28e148 README: Remove NVIDIA note 2024-09-13 15:10:25 +03:00
Ivan Molodetskikh 7a65a0b79f wiki: Delete unstable JSON output note 2024-09-13 15:06:20 +03:00
Ivan Molodetskikh ca30315deb Set rust-version in Cargo.toml 2024-09-13 15:05:41 +03:00
Ivan Molodetskikh 9538e8f916 Upgrade dependencies 2024-09-13 15:05:33 +03:00
Ivan Molodetskikh 8b3715eabf Update Smithay 2024-09-13 14:59:32 +03:00
Ivan Molodetskikh d0f2b9abd0 Fix formatting 2024-09-12 20:54:44 +03:00
Ivan Molodetskikh 43578e21b1 Always clamp non-auto window height with >1 windows in column 2024-09-12 19:31:47 +03:00
Ivan Molodetskikh 55a798bd8b Prevent unintended focus-follows-mouse during workspace switch 2024-09-12 16:48:29 +03:00
Ivan Molodetskikh cdcd5a2835 Update comments 2024-09-12 13:36:08 +03:00
Ivan Molodetskikh 737e99ec69 Add preset window heights to wiki & default config 2024-09-12 02:32:44 -07:00
Ivan Molodetskikh c3cb42f04d Add SwitchPresetWindowHeight by id 2024-09-12 02:32:44 -07:00
Christian Rieger d0e624e615 Implement preset window heights 2024-09-12 02:32:44 -07:00
Ivan Molodetskikh 087a50a19c wiki/Xwayland: Add note about existing DISPLAY 2024-09-10 11:33:08 +03:00
Ivan Molodetskikh 0bed253835 tty: Try connecting with invalid modifier on fail 2024-09-10 11:12:24 +03:00
Ivan Molodetskikh 6b6a84e55b Avoid panics on more wrong VBlank events 2024-09-10 10:48:45 +03:00
Ivan Molodetskikh 7d5785e96f Give focus to on-demand layer surfaces on map 2024-09-10 10:14:34 +03:00
Ivan Molodetskikh 70fa38fadf Possibly fix some unsync subsurfaces not redrawing output 2024-09-10 09:52:31 +03:00
Ivan Molodetskikh 3514cd2e36 Prefer exclusive layer focus to on-demand on the same layer 2024-09-10 09:10:03 +03:00
Ivan Molodetskikh 96083847fb ipc: Clarify some things in the docs 2024-09-09 08:51:03 +03:00
Ivan Molodetskikh d25d6ce337 Arrange layer map after sending new scale/transform
I think that should be a slightly better ordering of events.
2024-09-08 22:33:09 +03:00
Ivan Molodetskikh bb044075fa Inform layer surfaces of scale/transform changes
How'd I miss this and then never catch it?
2024-09-08 22:05:56 +03:00
Ivan Molodetskikh 370fd4e172 ipc: Convert all Action unit variants to unit struct variants
This is a breaking change, but likely nobody uses this through raw JSON
yet, and this allows us to add fields to any action later on without
another breaking change.
2024-09-06 18:32:51 +03:00
Ivan Molodetskikh 7dea3822a3 Fix set-window-height SetProportion scale 2024-09-06 18:32:51 +03:00
Ivan Molodetskikh 7d11ef0abb Extract print_window() 2024-09-06 18:32:51 +03:00
Ivan Molodetskikh dcb29efce5 Implement by-id window addressing in IPC and CLI, fix move-column-to-workspace
This is a JSON-breaking change for the IPC actions that changed from
unit variants to struct variants. Unfortunately, I couldn't find a way
with serde to both preserve a single variant, and make it serialize to
the old value when the new field is None. I don't think anyone is using
these actions from JSON at the moment, so this breaking change is fine.
2024-09-06 18:32:41 +03:00
Ivan Molodetskikh cb5d97f600 Fix new Clippy warning
This was stabilized in 1.76 so we can use it now.
2024-09-05 20:40:11 +03:00
Ivan Molodetskikh 608ab7d8b1 Change output sorting to match make/model/serial first
We can do this now that we have libdisplay-info.
2024-09-05 20:10:01 +03:00
elkowar fd8ebb9d06 implement always_center_single_column layout option 2024-09-05 01:01:41 -07:00
Ivan Molodetskikh 952916fd1c layout: Prevent view gesture snap beyond first/last column 2024-09-04 21:46:08 +03:00
Ivan Molodetskikh a0592e8f53 layout: Extract snap_points() 2024-09-04 21:45:47 +03:00
Ivan Molodetskikh 5460c792bd Fix missing KeyboardLayoutSwitched event on XKB switch 2024-09-04 20:54:11 +03:00
sodiboo e5ecd27bbe flake: add libdisplay-info to buildInputs 2024-09-04 09:39:22 -07:00
Ivan Molodetskikh 4543873dae wiki/IPC: Link to the online rustdoc 2024-09-04 13:15:43 +03:00
Ivan Molodetskikh a2c855315c ci: Add niri-ipc rustdoc generation 2024-09-04 12:39:23 +03:00
Ivan Molodetskikh 6c4e4b374a ipc: Write some more docs 2024-09-04 12:29:26 +03:00
Ivan Molodetskikh 9ab887bec8 ipc: Don't re-export socket types 2024-09-04 12:03:13 +03:00
Ivan Molodetskikh 268591f343 wiki: Add Since note to other open-on-output properties 2024-09-03 14:36:23 +03:00
Ivan Molodetskikh a42717bcac wiki/Xwayland: Mention adding DISPLAY to config environment 2024-09-03 14:12:28 +03:00
Ivan Molodetskikh 6b013a08fc wiki: Update package list 2024-09-03 13:51:24 +03:00
Ivan Molodetskikh b65a243fc9 Remove warning about missing output config 2024-09-03 13:48:08 +03:00
Ivan Molodetskikh f0157e03e7 Use libdisplay-info for make/model/serial parsing, implement throughout 2024-09-03 13:48:08 +03:00
Ivan Molodetskikh 4b7c16b04a Read config from /etc/niri/config.kdl too 2024-09-02 13:10:45 +03:00
Ivan Molodetskikh aafd5ab70f wiki: Use $NIRI_SOCKET in example 2024-09-02 12:38:33 +03:00
Ivan Molodetskikh d8d6b5a5e0 wiki: Fix niri-ipc links 2024-09-02 10:05:59 +03:00
Ivan Molodetskikh a1fd4b396f wiki: Fix code block formatting 2024-09-02 10:04:43 +03:00
Ivan Molodetskikh 5521cdda63 wiki: Add the word IPC to the sidebar 2024-09-02 10:03:44 +03:00
Ivan Molodetskikh 12b16a9d7e wiki: Document IPC programmatic access 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh f7181fb066 Implement by-id workspace action addressing
It's not added to clap because there's no convenient mutually-exclusive
argument enum derive yet (to have either the current <REFERENCE> or an
--id <ID>). It's not added to config parsing because I don't see how it
could be useful there. As such, it's only accessible through raw IPC.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 17ac52e1d4 Fix spelling mistake 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 64a9351921 Add niri msg windows 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 332af8b062 Rearrange some CLI and IPC enum values 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh b7901579d5 Change IdCounter to be backed by an AtomicU64
Let's see if anyone complains.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 138c2a3bfd Change OutputId::get() to return u64 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 446a9f1e06 Make WorkspaceId inner field private 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 52265e2e19 utils/id: Use a Relaxed atomic op 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 0f522f209b Change MappedIt::get() to return u64 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 30b213601a Implement the event stream IPC 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 8eb34b2e18 Animate focus-workspace by idx/back and forth/previous
Deleting the test because it only made sense when no-animation was
special cased.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 74d1b1f406 layout: Cache monitor output name 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 2b3d196876 Remove unused function 2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 397b7e4bb9 ipc: Read only a single line on the client
Allow extensibility.
2024-09-01 23:47:19 -07:00
Ivan Molodetskikh 598b27f83c flake: Remove maintainer comment
Effectively other contributors maintain it now.
2024-08-26 18:15:39 +03:00
Ivan Molodetskikh da53e79d07 wiki: Add hotkey overlay skip to FAQ 2024-08-26 10:35:00 +03:00
Ivan Molodetskikh 2907d5af3e wiki: Mark FAQ snippet as KDL 2024-08-26 10:35:00 +03:00
sodiboo dd919fe01b fix cargo run on nixos
this boils down to adding some extra dependencies to the shell
environment. they're also inherited from craneArgs because the ones from
the package are actually transformed into the WRONG outputs of the
packages. also refactors to use craneLib.devShell because it's somewhat
cleaner.
2024-08-25 15:42:25 +03:00
Ivan Molodetskikh f86a9bed1a layout: Break out early on min size 2024-08-25 11:46:04 +03:00
Ivan Molodetskikh cfa87d508e layout: Fix rounding in height distribution
Rounding before checking min height could artificially increase the
window height that we check, leading to an incorrectly satisfied min
constraint.
2024-08-25 10:16:37 +03:00
Ivan Molodetskikh f19e1711a7 Add niri msg keyboard-layouts 2024-08-25 09:38:45 +03:00
Ivan Molodetskikh 20cd4f5d04 layout: Clamp window height to max available in column
When the window is alone in its column this logic intentionally isn't
triggered. Until we have a floating layer, there's no other way to get a
window larger than the screen, which I need.
2024-08-25 08:46:34 +03:00
Ivan Molodetskikh b2c7d3ad40 Rework PW screencast frame timing
- Remove the 0.5 ms hack.
- Add redraw scheduling to fix stuck frame if the last redrawn frame
  happened too soon.
2024-08-24 10:49:32 +03:00
Ivan Molodetskikh 4832924483 Update Smithay (layer-shell popup fix) 2024-08-24 07:22:57 +03:00
Ivan Molodetskikh 28a8a9ace2 Register deadline timer for closing transaction 2024-08-23 19:09:18 +03:00
Ivan Molodetskikh a4f1caab1d wiki: Update transaction list 2024-08-23 15:53:01 +03:00
Ivan Molodetskikh c8839f7658 Implement window close transaction
Mainly visible with disabled animations.
2024-08-23 15:41:06 +03:00
Ivan Molodetskikh dfe3580607 animation: Use saturating_sub in value() 2024-08-23 15:39:57 +03:00
Ivan Molodetskikh 1c02552e92 animation: Make restarted() take by-ref 2024-08-23 15:39:45 +03:00
Ivan Molodetskikh ff7cbb97df Fix screen transition across scale/transform changes 2024-08-23 12:54:07 +03:00
Ivan Molodetskikh 09f3d3fb12 Extract Niri::update_render_elements() 2024-08-23 12:54:07 +03:00
Ivan Molodetskikh 63defc25d2 Fix Clippy warnings 2024-08-23 12:21:47 +03:00
Ivan Molodetskikh db39fc95f4 pw_utils: Re-create damage tracker on scale change 2024-08-23 11:14:24 +03:00
Ivan Molodetskikh 471dc714aa Add damage check to PW screencasts
Avoids unnecessary frames.
2024-08-23 11:02:34 +03:00
Ivan Molodetskikh fef665df73 tty: Wait for sync on needs_sync()
How did I never add this back?
2024-08-23 09:26:42 +03:00
Ivan Molodetskikh 7bfdf87bf0 Implement resize transactions 2024-08-22 15:19:11 +03:00
Ivan Molodetskikh cf357d7058 Implement window resize throttling 2024-08-22 14:40:40 +03:00
Ivan Molodetskikh 618fa08aa5 Update Smithay (apply state in post commit) 2024-08-22 14:15:04 +03:00
Ivan Molodetskikh a40e7b4470 Handle dmabuf blocker separately in toplevel pre-commit
Will be needed for transactions.
2024-08-22 13:13:28 +03:00
Michael Yang f1894f6f9a feature: add on-demand vrr (#586)
* feature: add on-demand vrr

* Don't require connector::Info in try_to_set_vrr

* Improve VRR help message

* Rename connector_handle => connector

* Fix tracy span name

* Move on demand vrr flag set higher

* wiki: Mention on-demand VRR

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-08-22 11:58:07 +03:00
Ivan Molodetskikh dfc2d452c5 layout: Do not recompute total_weight every iteration 2024-08-15 11:46:13 +03:00
Ivan Molodetskikh 66f23c3980 layout: Implement weighted height distribution
The intention is to make columns add up to the working area height most
of the time, while still preserving the ability to have one fixed-height
window.

Automatic heights are now distributed according to their weight, rather
than evenly. This is similar to flex-grow in CSS or fraction in Typst.

Resizing one window in a column still makes that window fixed, however
it changes all other windows to automatic height, computing their
weights in such a way as to preserve their apparent heights.
2024-08-15 10:50:38 +03:00
Ivan Molodetskikh 7a6ab31ad7 layout: Pre-subtract gaps during height distribution
Same result, but code a bit clearer.
2024-08-15 10:46:39 +03:00
Ivan Molodetskikh 2f73dd5b59 wiki: Use real em-dash 2024-08-14 18:33:43 +03:00
Ivan Molodetskikh c658424c9f wiki: Document invisible state 2024-08-14 18:32:50 +03:00
Ivan Molodetskikh bb58f2d162 wiki: Clarify named workspaces example 2024-08-14 18:18:05 +03:00
Fea f54297f242 flake: Update flake inputs 2024-08-14 10:49:54 +03:00
Fea b72d946062 Fix nix build 2024-08-14 10:49:54 +03:00
Ivan Molodetskikh 883763c172 Implement stub mutter-x11-interop
Allows xdp-gnome dialogs to work with X11 clients.

Fixes https://github.com/YaLTeR/niri/issues/594
2024-08-13 09:15:57 +03:00
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
143 changed files with 18713 additions and 5125 deletions
+36 -13
View File
@@ -34,7 +34,7 @@ jobs:
- 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
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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -85,7 +85,7 @@ jobs:
- 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
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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -98,7 +98,7 @@ jobs:
strategy:
fail-fast: false
name: 'msrv - 1.72.0'
name: 'msrv - 1.77.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
@@ -110,9 +110,9 @@ jobs:
- 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
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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@1.72.0
- uses: dtolnay/rust-toolchain@1.77.0
- uses: Swatinem/rust-cache@v2
@@ -134,7 +134,7 @@ jobs:
- 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
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 libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -153,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
@@ -174,7 +172,7 @@ 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 libadwaita-devel
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 libdisplay-info-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
@@ -194,7 +192,7 @@ jobs:
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- run: nix build
- run: nix flake check
continue-on-error: true
publish-wiki:
@@ -209,3 +207,28 @@ jobs:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
rustdoc:
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: dtolnay/rust-toolchain@stable
- name: Generate documentation
run: cargo doc --no-deps -p niri-ipc
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
- name: Deploy documentation
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
force_orphan: true
Generated
+1177 -703
View File
File diff suppressed because it is too large Load Diff
+54 -31
View File
@@ -2,22 +2,24 @@
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.6"
version = "0.1.9"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.83"
bitflags = "2.5.0"
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
anyhow = "1.0.90"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.132"
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.0", default-features = false }
tracy-client = { version = "0.17.4", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -36,46 +38,53 @@ authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow.workspace = true
arrayvec = "0.7.4"
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "1.13.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.16.0", features = ["derive"] }
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
bytemuck = { version = "1.19.0", features = ["derive"] }
calloop = { version = "0.14.1", 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"] }
drm-ffi = "0.9.0"
fastrand = "2.1.1"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.27.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
glam = "0.29.0"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.154"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.6", path = "niri-config" }
niri-ipc = { version = "0.1.6", path = "niri-ipc", features = ["clap"] }
libc = "0.2.161"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.9", path = "niri-config" }
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
pangocairo = "0.19.2"
pipewire = { version = "0.8.0", optional = true }
png = "0.17.13"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.1"
ordered-float = "4.4.0"
pango = { version = "0.20.4", features = ["v1_44"] }
pangocairo = "0.20.4"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.14"
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.3"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
url = { version = "2.5.2", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
xcursor = "0.3.8"
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
@@ -97,9 +106,10 @@ features = [
]
[dev-dependencies]
k9 = "0.12.0"
proptest = "1.4.0"
proptest-derive = "0.4.0"
approx = "0.5.1"
k9.workspace = true
proptest = "1.5.0"
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
xshell = "0.2.6"
[features]
@@ -112,6 +122,8 @@ systemd = ["dbus"]
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 = []
@@ -125,7 +137,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.6"
version = "0.1.9"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -137,3 +149,14 @@ 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"],
]
+18 -5
View File
@@ -7,7 +7,7 @@
</p>
<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/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)
@@ -31,10 +31,11 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- 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
@@ -48,8 +49,6 @@ A lot of the essential functionality is implemented, plus some goodies on top.
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.
Note that NVIDIA GPUs may have issues.
## Inspiration
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
@@ -57,6 +56,16 @@ 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.
## Tile Scrollably Elsewhere
Here are some other projects which implement a similar workflow:
- [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
@@ -64,4 +73,8 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[PaperWM]: https://github.com/paperwm/PaperWM
[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
+21 -95
View File
@@ -1,72 +1,12 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1709274179,
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -77,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
"type": "github"
},
"original": {
@@ -93,42 +33,28 @@
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-analyzer-src": {
"flake": false,
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709219524,
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
"lastModified": 1727663505,
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
+224 -81
View File
@@ -1,108 +1,251 @@
# This flake file is community maintained
# Maintainers:
# Bill Sun (github/billksun)
{
description = "Niri: A scrollable-tiling Wayland compositor.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nix-filter.url = "github:numtide/nix-filter";
fenix = {
url = "github:nix-community/fenix/monthly";
# NOTE: This is not necessary for end users
# You can omit it with `inputs.rust-overlay.follows = ""`
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
crane,
nix-filter,
flake-utils,
fenix,
...
}: let
systems = ["aarch64-linux" "x86_64-linux"];
in
flake-utils.lib.eachSystem systems (
system: let
pkgs = nixpkgs.legacyPackages.${system};
toolchain = fenix.packages.${system}.complete.toolchain;
craneLib = crane.lib.${system}.overrideToolchain toolchain;
outputs =
{
self,
nixpkgs,
nix-filter,
rust-overlay,
}:
let
niri-package =
{
lib,
cairo,
clang,
dbus,
libGL,
libclang,
libdisplay-info,
libinput,
libseat,
libxkbcommon,
mesa,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
withDbus ? true,
withSystemd ? true,
withScreencastSupport ? true,
withDinit ? false,
}:
craneArgs = {
rustPlatform.buildRustPackage {
pname = "niri";
version = self.rev or "dirty";
version = self.shortRev or self.dirtyShortRev or "unknown";
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));
src = nix-filter.lib.filter {
root = self;
include = [
"niri-config"
"niri-ipc"
"niri-visual-tests"
"resources"
"src"
./Cargo.lock
./Cargo.toml
];
};
nativeBuildInputs = with pkgs; [
pkg-config
autoPatchelfHook
postPatch = ''
patchShebangs resources/niri-session
substituteInPlace resources/niri.service \
--replace-fail '/usr/bin' "$out/bin"
'';
cargoLock = {
# NOTE: This is only used for Git dependencies
allowBuiltinFetchGit = true;
lockFile = ./Cargo.lock;
};
strictDeps = true;
nativeBuildInputs = [
clang
gdk-pixbuf
graphene
gtk4
libadwaita
pkg-config
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
];
buildInputs =
[
cairo
dbus
libGL
libdisplay-info
libinput
libseat
libxkbcommon
mesa # libgbm
pango
wayland
]
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
++ lib.optional withScreencastSupport pipewire
# Also includes libudev
++ lib.optional withSystemd systemd;
runtimeDependencies = with pkgs; [
wayland
mesa
libglvnd # For libEGL
xorg.libXcursor
xorg.libXi
];
buildFeatures =
lib.optional withDbus "dbus"
++ lib.optional withDinit "dinit"
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
++ lib.optional withSystemd "systemd";
buildNoDefaultFeatures = true;
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
postInstall =
''
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
''
+ lib.optionalString withSystemd ''
install -Dm755 resources/niri-session $out/bin/niri-session
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
'';
env = {
LIBCLANG_PATH = lib.getLib libclang + "/lib";
# Force linking with libEGL and libwayland-client
# so they can be discovered by `dlopen()`
CARGO_BUILD_RUSTFLAGS = toString (
map (arg: "-C link-arg=" + arg) [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
]
);
};
passthru = {
providedSessions = [ "niri" ];
};
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/YaLTeR/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
};
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
in {
formatter = pkgs.alejandra;
inherit (nixpkgs) lib;
# Support all Linux systems that the nixpkgs flake exposes
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
checks.niri = niri;
packages.default = niri;
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
{
checks = forAllSystems (system: {
# We use the debug build here to save a bit of time
inherit (self.packages.${system}) niri-debug;
});
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
packages = niri.runtimeDependencies;
devShells = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
inherit (self.packages.${system}) niri;
in
{
default = pkgs.mkShell {
packages = [
# We don't use the toolchain from nixpkgs
# because we prefer a nightly toolchain
# and we *require* a nightly rustfmt
(rust-bin.selectLatestNightlyWith (
toolchain:
toolchain.default.override {
extensions = [
# includes already:
# rustc
# cargo
# rust-std
# rust-docs
# rustfmt-preview
# clippy-preview
"rust-analyzer"
"rust-src"
];
}
))
];
# Force linking to libEGL, which is always dlopen()ed, and to
# libwayland-client, which is always dlopen()ed except by the
# obscure winit backend.
RUSTFLAGS = map (a: "-C link-arg=${a}") [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
];
};
}
);
nativeBuildInputs = [
pkgs.clang
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
buildInputs = niri.buildInputs ++ [
pkgs.libadwaita # For `niri-visual-tests`
];
env = {
inherit (niri) LIBCLANG_PATH;
# WARN: Do not overwrite this variable in your shell!
# It is required for `dlopen()` to work on some libraries; see the comment
# in the package expression
#
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
inherit (niri) CARGO_BUILD_RUSTFLAGS;
};
};
}
);
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
packages = forAllSystems (
system:
let
niri = nixpkgsFor.${system}.callPackage niri-package { };
in
{
inherit niri;
# NOTE: This is for development purposes only
#
# It is primarily to help with quickly iterating on
# changes made to the above expression - though it is
# also not stripped in order to better debug niri itself
niri-debug = niri.overrideAttrs (
newAttrs: oldAttrs: {
pname = oldAttrs.pname + "-debug";
cargoBuildType = "debug";
cargoCheckType = newAttrs.cargoBuildType;
dontStrip = true;
}
);
default = niri;
}
);
overlays.default = final: _: {
niri = final.callPackage niri-package { };
};
};
}
+8 -3
View File
@@ -9,11 +9,16 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.6.2"
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.6", path = "../niri-ipc" }
regex = "1.10.4"
niri-ipc = { version = "0.1.9", path = "../niri-ipc" }
regex = "1.11.0"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
[dev-dependencies]
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
+1162 -176
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")
);
}
}
+2
View File
@@ -9,8 +9,10 @@ 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"]
+404 -90
View File
@@ -1,4 +1,24 @@
//! Types for communicating with niri via IPC.
//!
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
//! can keep reading [`Event`]s from the socket after the response.
//!
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
//! you are encouraged to communicate with the socket manually.
//!
//! 1. Read the socket filesystem path from [`socket::SOCKET_PATH_ENV`] (`$NIRI_SOCKET`).
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! on a single line each.
//!
//! ## Backwards compatibility
//!
//! This crate follows the niri version. It is **not** API-stable in terms of the Rust semver. In
//! particular, expect new struct fields and enum variants to be added in patch version bumps.
#![warn(missing_docs)]
use std::collections::HashMap;
@@ -6,16 +26,25 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize};
mod socket;
pub use socket::{Socket, SOCKET_PATH_ENV};
pub mod socket;
pub mod state;
/// Request from client to niri.
#[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 workspaces.
Workspaces,
/// Request information about open windows.
Windows,
/// Request information about the configured keyboard layouts.
KeyboardLayouts,
/// Request information about the focused output.
FocusedOutput,
/// Request information about the focused window.
FocusedWindow,
/// Perform an action.
@@ -31,8 +60,21 @@ pub enum Request {
/// Configuration to apply.
action: OutputAction,
},
/// Request information about workspaces.
Workspaces,
/// Start continuously receiving events from the compositor.
///
/// The compositor should reply with `Reply::Ok(Response::Handled)`, then continuously send
/// [`Event`]s, one per line.
///
/// The event stream will always give you the full current state up-front. For example, the
/// first workspace-related event you will receive will be [`Event::WorkspacesChanged`]
/// containing the full current workspaces state. You *do not* need to separately send
/// [`Request::Workspaces`] when using the event stream.
///
/// Where reasonable, event stream state updates are atomic, though this is not always the
/// case. For example, a window may end up with a workspace id for a workspace that had already
/// been removed. This can happen if the corresponding [`Event::WorkspacesChanged`] arrives
/// before the corresponding [`Event::WindowOpenedOrChanged`].
EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
}
@@ -49,6 +91,7 @@ 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,
@@ -56,14 +99,20 @@ pub enum Response {
Version(String),
/// Information about connected outputs.
///
/// Map from connector name to output info.
/// Map from output name to output info.
Outputs(HashMap<String, Output>),
/// Information about workspaces.
Workspaces(Vec<Workspace>),
/// Information about open windows.
Windows(Vec<Window>),
/// Information about the keyboard layout.
KeyboardLayouts(KeyboardLayouts),
/// Information about the focused output.
FocusedOutput(Option<Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about workspaces.
Workspaces(Vec<Workspace>),
}
/// Actions that niri can perform.
@@ -73,6 +122,7 @@ pub enum Response {
#[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 {
@@ -81,7 +131,9 @@ pub enum Action {
skip_confirmation: bool,
},
/// Power off all monitors via DPMS.
PowerOffMonitors,
PowerOffMonitors {},
/// Power on all monitors via DPMS.
PowerOnMonitors {},
/// Spawn a command.
Spawn {
/// Command to spawn.
@@ -95,61 +147,135 @@ pub enum Action {
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot,
Screenshot {},
/// Screenshot the focused screen.
ScreenshotScreen,
/// Screenshot the focused window.
ScreenshotWindow,
/// Close the focused window.
CloseWindow,
/// Toggle fullscreen on the focused window.
FullscreenWindow,
ScreenshotScreen {},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
ScreenshotWindow {
/// Id of the window to screenshot.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Close a window.
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
CloseWindow {
/// Id of the window to close.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle fullscreen on a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle fullscreen on the focused window")
)]
FullscreenWindow {
/// Id of the window to toggle fullscreen of.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus a window by id.
FocusWindow {
/// Id of the window to focus.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus the column to the left.
FocusColumnLeft,
FocusColumnLeft {},
/// Focus the column to the right.
FocusColumnRight,
FocusColumnRight {},
/// Focus the first column.
FocusColumnFirst,
FocusColumnFirst {},
/// Focus the last column.
FocusColumnLast,
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,
FocusWindowDown {},
/// Focus the window above.
FocusWindowUp,
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,
FocusWindowOrWorkspaceDown {},
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp,
FocusWindowOrWorkspaceUp {},
/// Move the focused column to the left.
MoveColumnLeft,
MoveColumnLeft {},
/// Move the focused column to the right.
MoveColumnRight,
MoveColumnRight {},
/// Move the focused column to the start of the workspace.
MoveColumnToFirst,
MoveColumnToFirst {},
/// Move the focused column to the end of the workspace.
MoveColumnToLast,
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,
MoveWindowDown {},
/// Move the focused window up in a column.
MoveWindowUp,
MoveWindowUp {},
/// Move the focused window down in a column or to the workspace below.
MoveWindowDownOrToWorkspaceDown,
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,
MoveWindowUpOrToWorkspaceUp {},
/// Consume or expel a window left.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window left")
)]
ConsumeOrExpelWindowLeft {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume or expel a window right.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window right")
)]
ConsumeOrExpelWindowRight {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn,
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn,
ExpelWindowFromColumn {},
/// Center the focused column on the screen.
CenterColumn,
CenterColumn {},
/// Focus the workspace below.
FocusWorkspaceDown,
FocusWorkspaceDown {},
/// Focus the workspace above.
FocusWorkspaceUp,
FocusWorkspaceUp {},
/// Focus a workspace by reference (index or name).
FocusWorkspace {
/// Reference (index or name) of the workspace to focus.
@@ -157,21 +283,31 @@ pub enum Action {
reference: WorkspaceReferenceArg,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
FocusWorkspacePrevious {},
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceDown {},
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by reference (index or name).
MoveWindowToWorkspaceUp {},
/// Move a window to a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused window to a workspace by reference (index or name)")
)]
MoveWindowToWorkspace {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
window_id: Option<u64>,
/// 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,
MoveColumnToWorkspaceDown {},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
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.
@@ -179,45 +315,73 @@ pub enum Action {
reference: WorkspaceReferenceArg,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp,
MoveWorkspaceUp {},
/// Focus the monitor to the left.
FocusMonitorLeft,
FocusMonitorLeft {},
/// Focus the monitor to the right.
FocusMonitorRight,
FocusMonitorRight {},
/// Focus the monitor below.
FocusMonitorDown,
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp,
FocusMonitorUp {},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft,
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
MoveWindowToMonitorRight,
MoveWindowToMonitorRight {},
/// Move the focused window to the monitor below.
MoveWindowToMonitorDown,
MoveWindowToMonitorDown {},
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp,
MoveWindowToMonitorUp {},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft,
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
MoveColumnToMonitorRight,
MoveColumnToMonitorRight {},
/// Move the focused column to the monitor below.
MoveColumnToMonitorDown,
MoveColumnToMonitorDown {},
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp,
/// Change the height of the focused window.
MoveColumnToMonitorUp {},
/// Change the height of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Change the height of the focused window")
)]
SetWindowHeight {
/// Id of the window whose height to set.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Reset the height of the focused window back to automatic.
ResetWindowHeight,
/// Reset the height of a window back to automatic.
#[cfg_attr(
feature = "clap",
clap(about = "Reset the height of the focused window back to automatic")
)]
ResetWindowHeight {
/// Id of the window whose height to reset.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset column widths.
SwitchPresetColumnWidth,
SwitchPresetColumnWidth {},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle the maximized state of the focused column.
MaximizeColumn,
MaximizeColumn {},
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
@@ -231,25 +395,26 @@ pub enum Action {
layout: LayoutSwitchTarget,
},
/// Show the hotkey overlay.
ShowHotkeyOverlay,
ShowHotkeyOverlay {},
/// Move the focused workspace to the monitor to the left.
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorLeft {},
/// Move the focused workspace to the monitor to the right.
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorRight {},
/// Move the focused workspace to the monitor below.
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorDown {},
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp,
MoveWorkspaceToMonitorUp {},
/// Toggle a debug tint on windows.
ToggleDebugTint,
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions,
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage,
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),
@@ -261,9 +426,12 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Workspace reference (index or name) to operate on.
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum WorkspaceReferenceArg {
/// Id of the workspace.
Id(u64),
/// Index of the workspace.
Index(u8),
/// Name of the workspace.
@@ -272,6 +440,7 @@ pub enum WorkspaceReferenceArg {
/// 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,
@@ -286,6 +455,7 @@ pub enum LayoutSwitchTarget {
#[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,
@@ -295,7 +465,7 @@ pub enum OutputAction {
Mode {
/// Mode to set, or "auto" for automatic selection.
///
/// Run `niri msg outputs` to see the avaliable modes.
/// Run `niri msg outputs` to see the available modes.
#[cfg_attr(feature = "clap", arg())]
mode: ModeToSet,
},
@@ -317,23 +487,17 @@ pub enum OutputAction {
#[cfg_attr(feature = "clap", command(subcommand))]
position: PositionToSet,
},
/// Toggle variable refresh rate.
/// Set the variable refresh rate mode.
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,
/// Variable refresh rate mode to set.
#[cfg_attr(feature = "clap", command(flatten))]
vrr: VrrToSet,
},
}
/// 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,
@@ -343,6 +507,7 @@ pub enum ModeToSet {
/// 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,
@@ -354,6 +519,7 @@ pub struct ConfiguredMode {
/// 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,
@@ -366,6 +532,7 @@ pub enum ScaleToSet {
#[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"))]
@@ -378,6 +545,7 @@ pub enum PositionToSet {
/// 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,
@@ -385,8 +553,30 @@ pub struct ConfiguredPosition {
pub y: i32,
}
/// Output VRR to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct VrrToSet {
/// 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(),
hide_possible_values = true,
),
)]
pub vrr: bool,
/// Only enable when the output shows a window matching the variable-refresh-rate window rule.
#[cfg_attr(feature = "clap", arg(long))]
pub on_demand: bool,
}
/// 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,
@@ -394,6 +584,8 @@ pub struct Output {
pub make: String,
/// Textual description of the model.
pub model: String,
/// Serial of the output, if known.
pub serial: Option<String>,
/// Physical width and height of the output in millimeters, if known.
pub physical_size: Option<(u32, u32)>,
/// Available modes for the output.
@@ -413,7 +605,8 @@ pub struct Output {
}
/// 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,
@@ -426,7 +619,8 @@ pub struct Mode {
}
/// Logical output in the compositor's coordinate space.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct LogicalOutput {
/// Logical X position.
pub x: i32,
@@ -445,6 +639,7 @@ pub struct LogicalOutput {
/// 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,
@@ -472,15 +667,31 @@ pub enum Transform {
/// Toplevel window.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Window {
/// Unique id of this window.
///
/// This id remains constant while this window is open.
///
/// Do not assume that window ids will always increase without wrapping, or start at 1. That is
/// an implementation detail subject to change. For example, ids may change to be randomly
/// generated for each new window.
pub id: u64,
/// Title, if set.
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
/// Id of the workspace this window is on, if any.
pub workspace_id: Option<u64>,
/// Whether this window is currently focused.
///
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
pub is_focused: bool,
}
/// 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,
@@ -490,10 +701,24 @@ pub enum OutputConfigChanged {
/// A workspace.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Workspace {
/// Unique id of this workspace.
///
/// This id remains constant regardless of the workspace moving around and across monitors.
///
/// Do not assume that workspace ids will always increase without wrapping, or start at 1. That
/// is an implementation detail subject to change. For example, ids may change to be randomly
/// generated for each new workspace.
pub id: u64,
/// Index of the workspace on its monitor.
///
/// This is the same index you can use for requests like `niri msg action focus-workspace`.
///
/// This index *will change* as you move and re-order workspace. It is merely the workspace's
/// current position on its monitor. Workspaces on different monitors can have the same index.
///
/// If you need a unique workspace id that doesn't change, see [`Self::id`].
pub idx: u8,
/// Optional name of the workspace.
pub name: Option<String>,
@@ -502,7 +727,96 @@ pub struct Workspace {
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
pub is_active: bool,
/// Whether the workspace is currently focused.
///
/// There's only one focused workspace across all outputs.
pub is_focused: bool,
/// Id of the active window on this workspace, if any.
pub active_window_id: Option<u64>,
}
/// Configured keyboard layouts.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct KeyboardLayouts {
/// XKB names of the configured layouts.
pub names: Vec<String>,
/// Index of the currently active layout in `names`.
pub current_idx: u8,
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum Event {
/// The workspace configuration has changed.
WorkspacesChanged {
/// The new workspace configuration.
///
/// This configuration completely replaces the previous configuration. I.e. if any
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
/// workspace on its output. All other workspaces on the same output become inactive.
WorkspaceActivated {
/// Id of the newly active workspace.
id: u64,
/// Whether this workspace also became focused.
///
/// If `true`, this is now the single focused workspace. All other workspaces are no longer
/// focused, but they may remain active on their respective outputs.
focused: bool,
},
/// An active window changed on a workspace.
WorkspaceActiveWindowChanged {
/// Id of the workspace on which the active window changed.
workspace_id: u64,
/// Id of the new active window, if any.
active_window_id: Option<u64>,
},
/// The window configuration has changed.
WindowsChanged {
/// The new window configuration.
///
/// This configuration completely replaces the previous configuration. I.e. if any windows
/// are missing from here, then they were closed.
windows: Vec<Window>,
},
/// A new toplevel window was opened, or an existing toplevel window changed.
WindowOpenedOrChanged {
/// The new or updated window.
///
/// If the window is focused, all other windows are no longer focused.
window: Window,
},
/// A toplevel window was closed.
WindowClosed {
/// Id of the removed window.
id: u64,
},
/// Window focus changed.
///
/// All other windows are no longer focused.
WindowFocusChanged {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
keyboard_layouts: KeyboardLayouts,
},
/// The keyboard layout switched.
KeyboardLayoutSwitched {
/// Index of the newly active layout.
idx: u8,
},
}
impl FromStr for WorkspaceReferenceArg {
@@ -513,7 +827,7 @@ impl FromStr for WorkspaceReferenceArg {
if let Ok(idx) = u8::try_from(index) {
Self::Index(idx)
} else {
return Err("workspace indexes must be between 0 and 255");
return Err("workspace index must be between 0 and 255");
}
} else {
Self::Name(s.to_string())
+23 -9
View File
@@ -1,12 +1,12 @@
//! Helper for blocking communication over the niri socket.
use std::env;
use std::io::{self, Read, Write};
use std::io::{self, BufRead, BufReader, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::{Reply, Request};
use crate::{Event, Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
@@ -47,17 +47,31 @@ impl Socket {
/// * `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> {
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_vec(&request).unwrap();
stream.write_all(&buf)?;
let mut buf = serde_json::to_string(&request).unwrap();
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
buf.clear();
stream.read_to_end(&mut buf)?;
let mut reader = BufReader::new(stream);
let reply = serde_json::from_slice(&buf)?;
Ok(reply)
buf.clear();
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
let events = move || {
buf.clear();
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}
+194
View File
@@ -0,0 +1,194 @@
//! Helpers for keeping track of the event stream state.
//!
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
//! you only care about part of the state.
//! 2. Connect to the niri socket and request an event stream.
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
//! 4. Read the fields of the state as needed.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use crate::{Event, KeyboardLayouts, Window, Workspace};
/// Part of the state communicated via the event stream.
pub trait EventStreamStatePart {
/// Returns a sequence of events that replicates this state from default initialization.
fn replicate(&self) -> Vec<Event>;
/// Applies the event to this state.
///
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
/// part of the state.
fn apply(&mut self, event: Event) -> Option<Event>;
}
/// The full state communicated over the event stream.
///
/// Different parts of the state are not guaranteed to be consistent across every single event
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
/// these two events, the workspace active window id refers to a window that does not yet exist in
/// the windows state part.
#[derive(Debug, Default)]
pub struct EventStreamState {
/// State of workspaces.
pub workspaces: WorkspacesState,
/// State of workspaces.
pub windows: WindowsState,
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
}
/// The workspaces state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WorkspacesState {
/// Map from a workspace id to the workspace.
pub workspaces: HashMap<u64, Workspace>,
}
/// The windows state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WindowsState {
/// Map from a window id to the window.
pub windows: HashMap<u64, Window>,
}
/// The keyboard layout state communicated over the event stream.
#[derive(Debug, Default)]
pub struct KeyboardLayoutsState {
/// Configured keyboard layouts.
pub keyboard_layouts: Option<KeyboardLayouts>,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events
}
fn apply(&mut self, event: Event) -> Option<Event> {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
Some(event)
}
}
impl EventStreamStatePart for WorkspacesState {
fn replicate(&self) -> Vec<Event> {
let workspaces = self.workspaces.values().cloned().collect();
vec![Event::WorkspacesChanged { workspaces }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
let output = ws.output.clone();
for ws in self.workspaces.values_mut() {
let got_activated = ws.id == id;
if ws.output == output {
ws.is_active = got_activated;
}
if focused {
ws.is_focused = got_activated;
}
}
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
let ws = self.workspaces.get_mut(&workspace_id);
let ws = ws.expect("changed workspace was missing from the map");
ws.active_window_id = active_window_id;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for WindowsState {
fn replicate(&self) -> Vec<Event> {
let windows = self.windows.values().cloned().collect();
vec![Event::WindowsChanged { windows }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WindowsChanged { windows } => {
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
}
Event::WindowOpenedOrChanged { window } => {
let (id, is_focused) = match self.windows.entry(window.id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
*entry = window;
(entry.id, entry.is_focused)
}
Entry::Vacant(entry) => {
let entry = entry.insert(window);
(entry.id, entry.is_focused)
}
};
if is_focused {
for win in self.windows.values_mut() {
if win.id != id {
win.is_focused = false;
}
}
}
}
Event::WindowClosed { id } => {
let win = self.windows.remove(&id);
win.expect("closed window was missing from the map");
}
Event::WindowFocusChanged { id } => {
for win in self.windows.values_mut() {
win.is_focused = Some(win.id) == id;
}
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for KeyboardLayoutsState {
fn replicate(&self) -> Vec<Event> {
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
} else {
vec![]
}
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
self.keyboard_layouts = Some(keyboard_layouts);
}
Event::KeyboardLayoutSwitched { idx } => {
let kb = self.keyboard_layouts.as_mut();
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
kb.current_idx = idx;
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.2", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.6", path = ".." }
niri-config = { version = "0.1.6", path = "../niri-config" }
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.9", path = ".." }
niri-config = { version = "0.1.9", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
@@ -4,7 +4,7 @@ use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::CornerRadius;
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};
@@ -59,17 +59,19 @@ impl TestCase for GradientAngle {
) -> 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);
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),
[1., 0., 0., 1.],
[0., 1., 0., 1.],
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),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
+14 -10
View File
@@ -5,10 +5,10 @@ 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};
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, Scale, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use super::TestCase;
@@ -22,8 +22,8 @@ impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
active_color: Color::new(255, 255, 255, 128),
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
@@ -75,13 +75,14 @@ impl TestCase for GradientArea {
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size);
let 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 = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
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;
@@ -91,10 +92,11 @@ impl TestCase for GradientArea {
true,
Rectangle::default(),
CornerRadius::default(),
1.,
);
rv.extend(
self.border
.render(renderer, Point::from(g_loc), Scale::from(1.))
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
@@ -102,12 +104,14 @@ impl TestCase for GradientArea {
[BorderRenderElement::new(
area.size,
g_area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
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),
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
@@ -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()
}
}
+14 -13
View File
@@ -5,7 +5,7 @@ use niri::layout::workspace::ColumnWidth;
use niri::layout::{LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::Color;
use niri_config::{Color, FloatOrInt, OutputName};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
@@ -41,6 +41,12 @@ impl Layout {
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
output.user_data().insert_if_missing(|| OutputName {
connector: String::new(),
make: None,
model: None,
serial: None,
});
let options = Options {
focus_ring: niri_config::FocusRing {
@@ -49,9 +55,9 @@ impl Layout {
},
border: niri_config::Border {
off: false,
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
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,
},
@@ -147,7 +153,7 @@ impl Layout {
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.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout.add_window(window.clone(), width, false);
@@ -161,7 +167,7 @@ impl Layout {
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout
@@ -192,11 +198,7 @@ impl TestCase for Layout {
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
@@ -222,12 +224,11 @@ impl TestCase for Layout {
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout.update_render_elements(&self.output);
self.layout.update_render_elements(Some(&self.output));
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
+11
View File
@@ -6,6 +6,17 @@ 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;
+12 -11
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri_config::Color;
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};
@@ -20,7 +20,7 @@ impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -28,7 +28,7 @@ impl Tile {
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -37,7 +37,7 @@ impl Tile {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -71,13 +71,13 @@ impl Tile {
},
border: niri_config::Border {
off: false,
width: 32,
active_color: Color::new(255, 163, 72, 255),
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(), Rc::new(options));
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
Self { window, tile }
}
}
@@ -85,7 +85,7 @@ impl Tile {
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
self.tile
.request_tile_size(Size::from((width, height)), false);
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
self.window.communicate();
}
@@ -102,12 +102,13 @@ impl TestCase for Tile {
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
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)),
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
);
self.tile
.render(
+8 -5
View File
@@ -14,14 +14,14 @@ pub struct Window {
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(size, false);
window.request_size(size, false, None);
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.request_size(size, false, None);
window.communicate();
Self { window }
}
@@ -29,7 +29,7 @@ impl 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.request_size(size, false, None);
window.communicate();
Self { window }
}
@@ -37,7 +37,8 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)), false);
self.window
.request_size(Size::from((width, height)), false, None);
self.window.communicate();
}
@@ -47,7 +48,9 @@ impl TestCase for Window {
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let win_size = self.window.size().to_physical(1);
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
let location = Point::from((size.w - win_size.w, size.h - win_size.h))
.to_f64()
.downscale(2.);
self.window
.render(
+24 -2
View File
@@ -5,8 +5,6 @@ use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::tile::Tile;
use cases::window::Window;
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
@@ -18,7 +16,20 @@ 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;
@@ -112,6 +123,17 @@ fn build_ui(app: &adw::Application) {
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();
+5 -10
View File
@@ -15,8 +15,8 @@ mod imp {
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
@@ -147,7 +147,7 @@ mod imp {
.context("error creating frame")?;
frame
.clear([0.3, 0.3, 0.3, 1.], &[rect])
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
.context("error clearing")?;
for element in elements.iter().rev() {
@@ -157,7 +157,7 @@ mod imp {
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.draw(&mut frame, src, dst, &[damage], &[])
.context("error drawing element")?;
}
}
@@ -186,13 +186,8 @@ mod imp {
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
.context("error creating EGL context")?;
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
.context("error getting supported renderer capabilities")?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")?;
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
resources::init(&mut renderer);
shaders::init(&mut renderer);
+30 -17
View File
@@ -3,14 +3,16 @@ use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
ConfigureIntent, 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::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
@@ -37,7 +39,7 @@ impl TestWindow {
let size = Size::from((100, 200));
let min_size = Size::from((0, 0));
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]);
Self {
id,
@@ -49,7 +51,7 @@ impl TestWindow {
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
})),
}
}
@@ -85,7 +87,7 @@ impl TestWindow {
let mut new_size = inner.size;
if let Some(size) = inner.requested_size.take() {
if let Some(size) = inner.requested_size {
assert!(size.w >= 0);
assert!(size.h >= 0);
@@ -112,14 +114,14 @@ impl TestWindow {
if inner.size != new_size {
inner.size = new_size;
inner.buffer.resize(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);
inner.csd_shadow_buffer.resize(csd_shadow_size.to_f64());
rv
}
@@ -147,8 +149,8 @@ impl LayoutElement for TestWindow {
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>> {
@@ -158,17 +160,15 @@ impl LayoutElement for TestWindow {
normal: vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
location,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
location
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
@@ -178,7 +178,12 @@ impl LayoutElement for TestWindow {
}
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
@@ -199,7 +204,7 @@ impl LayoutElement for TestWindow {
false
}
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
fn has_ssd(&self) -> bool {
false
@@ -217,6 +222,10 @@ impl LayoutElement for TestWindow {
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
@@ -227,6 +236,10 @@ impl LayoutElement for TestWindow {
self.inner.borrow().pending_fullscreen
}
fn requested_size(&self) -> Option<Size<i32, Logical>> {
self.inner.borrow().requested_size
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
+148
View File
@@ -0,0 +1,148 @@
%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: pkgconfig(libdisplay-info)
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 }}}
+42 -8
View File
@@ -21,25 +21,41 @@ input {
// 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"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
// 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
@@ -60,8 +76,8 @@ input {
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@120.030"
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// 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.
@@ -107,6 +123,9 @@ layout {
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
@@ -147,6 +166,7 @@ layout {
// 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
@@ -187,11 +207,14 @@ layout {
// 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"
// 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.
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
// This option will also fix border/focus ring drawing behind some semitransparent windows.
// After enabling or disabling this, you need to restart the apps for this to take effect.
// prefer-no-csd
// You can change the path where screenshots are saved.
@@ -239,6 +262,13 @@ window-rule {
// block-out-from "screencast"
}
// Example: enable rounded corners for all windows.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
binds {
// Keys consist of modifiers separated by + signs, followed by an XKB key name
// in the end. To find an XKB name for a particular key, you may use a program
@@ -259,7 +289,8 @@ binds {
Mod+D { spawn "fuzzel"; }
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.
@@ -410,15 +441,18 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// Consume one window from the right into the focused column.
Mod+Comma { consume-window-into-column; }
// Expel one window from the focused column to the right.
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+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+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
+8
View File
@@ -0,0 +1,8 @@
type = process
command = niri --session
restart = false
working-dir = $HOME
depends-on = dbus
after = niri-shutdown
chain-to = niri-shutdown
options: always-chain
+3
View File
@@ -0,0 +1,3 @@
type = scripted
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
restart = false
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="mutter_x11_interop">
<description summary="X11 interoperability helper">
This protocol is intended to be used by the portal backend to map Wayland
dialogs as modal dialogs on top of X11 windows.
</description>
<interface name="mutter_x11_interop" version="1">
<description summary="X11 interoperability helper"/>
<request name="destroy" type="destructor"/>
<request name="set_x11_parent">
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="xwindow" type="uint"/>
</request>
</interface>
</protocol>
+47 -27
View File
@@ -11,31 +11,51 @@ if [ -n "$SHELL" ] &&
fi
fi
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
exit 1
# Try to detect the service manager that is being used
if hash systemctl &> /dev/null; then
# Make sure there's no already running session.
if systemctl --user -q is-active niri.service; then
echo 'A niri session is already running.'
exit 1
fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Import the login manager environment.
systemctl --user import-environment
# DBus activation environment is independent from systemd. While most of
# dbus-activated services are already using `SystemdService` directive, some
# still don't and thus we should set the dbus environment with a separate
# command.
if hash dbus-update-activation-environment 2>/dev/null; then
dbus-update-activation-environment --all
fi
# Start niri and wait for it to terminate.
systemctl --user --wait start niri.service
# 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 NIRI_SOCKET
elif hash dinitctl &> /dev/null; then
# Check that the user dinit daemon is running
if ! pgrep -u $(id -u) dinit &> /dev/null; then
echo "dinit user daemon is not running."
exit 1
fi
# Make sure there's no already running session.
if dinitctl --user is-started niri &> /dev/null; then
echo 'A niri session is already running.'
exit 1
fi
# Start niri
dinitctl --user start niri
else
echo "No systemd or dinit detected, please use niri --session instead."
fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Import the login manager environment.
systemctl --user import-environment
# DBus activation environment is independent from systemd. While most of
# dbus-activated services are already using `SystemdService` directive, some
# still don't and thus we should set the dbus environment with a separate
# command.
if hash dbus-update-activation-environment 2>/dev/null; then
dbus-update-activation-environment --all
fi
# Start niri and wait for it to terminate.
systemctl --user --wait start niri.service
# Force stop of grahical-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 NIRI_SOCKET
+6
View File
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
</head>
</html>
+4 -4
View File
@@ -11,7 +11,7 @@ pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Animation {
from: f64,
to: f64,
@@ -101,9 +101,9 @@ impl Animation {
}
/// Restarts the animation using the previous config.
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
if self.is_off {
return self;
return self.clone();
}
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
@@ -292,7 +292,7 @@ impl Animation {
return self.to;
}
let passed = self.current_time - self.start_time;
let passed = self.current_time.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
+32 -1
View File
@@ -9,6 +9,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::Niri;
use crate::utils::id::IdCounter;
pub mod tty;
pub use tty::Tty;
@@ -31,7 +32,22 @@ pub enum RenderResult {
Skipped,
}
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
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(u64);
impl OutputId {
fn next() -> OutputId {
OutputId(OUTPUT_ID_COUNTER.next())
}
pub fn get(self) -> u64 {
self.0
}
}
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
@@ -137,6 +153,13 @@ impl Backend {
}
}
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
match self {
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
Backend::Winit(_) => (),
}
}
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
@@ -151,6 +174,14 @@ impl Backend {
}
}
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
if let Self::Tty(v) = self {
Some(v)
} else {
None
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v
+380 -227
View File
@@ -4,7 +4,6 @@ use std::fmt::Write;
use std::iter::zip;
use std::num::NonZeroU64;
use std::os::fd::AsFd;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -14,18 +13,19 @@ use std::{io, mem};
use anyhow::{anyhow, bail, ensure, Context};
use bytemuck::cast_slice_mut;
use libc::dev_t;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::{Format, Fourcc};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
use smithay::backend::drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
};
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::egl::{EGLDevice, EGLDisplay};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -51,11 +51,11 @@ use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use super::{IpcOutputMap, RenderResult};
use crate::backend::OutputId;
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
@@ -117,6 +117,7 @@ pub struct OutputDevice {
render_node: DrmNode,
drm_scanner: DrmScanner,
surfaces: HashMap<crtc::Handle, Surface>,
output_ids: HashMap<crtc::Handle, OutputId>,
// SAFETY: drop after all the objects used with them are dropped.
// See https://github.com/Smithay/smithay/issues/1102.
drm: DrmDevice,
@@ -145,7 +146,16 @@ impl OutputDevice {
builder.add_connector(connector);
builder.add_crtc(*crtc);
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
builder.add_plane(planes.primary.handle);
let (primary_plane, primary_plane_claim) = planes
.primary
.iter()
.find_map(|plane| {
self.drm
.claim_plane(plane.handle, *crtc)
.map(|claim| (plane, claim))
})
.ok_or_else(LeaseRejected::default)?;
builder.add_plane(primary_plane.handle, primary_plane_claim);
}
Ok(builder)
}
@@ -166,8 +176,9 @@ struct TtyOutputState {
}
struct Surface {
name: String,
name: OutputName,
compositor: GbmDrmCompositor,
connector: connector::Handle,
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
@@ -236,25 +247,7 @@ impl Tty {
})
.unwrap();
let config_ = config.clone();
let create_renderer = move |display: &EGLDisplay| {
let color_transforms = config_
.borrow()
.debug
.enable_color_transformations_capability;
let egl_context = EGLContext::new_with_priority(display, ContextPriority::High)?;
let gles = if color_transforms {
unsafe { GlesRenderer::new(egl_context)? }
} else {
let capabilities = unsafe { GlesRenderer::supported_capabilities(&egl_context) }?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
unsafe { GlesRenderer::with_capabilities(egl_context, capabilities)? }
};
Ok(gles)
};
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
let api = GbmGlesBackend::with_context_priority(ContextPriority::High);
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
@@ -411,6 +404,8 @@ impl Tty {
self.device_changed(node.dev_id(), niri);
// Apply pending gamma changes and restore our existing gamma.
//
// Also, restore our VRR.
let device = self.devices.get_mut(&node).unwrap();
for (crtc, surface) in device.surfaces.iter_mut() {
if let Some(ramp) = surface.pending_gamma_change.take() {
@@ -428,6 +423,33 @@ impl Tty {
warn!("error restoring gamma: {err:?}");
}
}
// Restore VRR.
let output = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == node && tty_state.crtc == *crtc
})
.cloned();
let Some(output) = output else {
error!("missing output for crtc: {crtc:?}");
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name.connector);
continue;
};
try_to_change_vrr(
&device.drm,
surface.connector,
*crtc,
surface,
output_state,
surface.vrr_enabled,
);
}
}
@@ -516,7 +538,7 @@ impl Tty {
niri.layout.update_shaders();
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats().collect::<HashSet<_>>();
let primary_formats = renderer.dmabuf_formats();
let default_feedback =
DmabufFeedbackBuilder::new(render_node.dev_id(), primary_formats.clone())
.build()
@@ -574,6 +596,7 @@ impl Tty {
gbm,
drm_scanner: DrmScanner::new(),
surfaces: HashMap::new(),
output_ids: HashMap::new(),
drm_lease_state,
active_leases: Vec::new(),
non_desktop_connectors: HashSet::new(),
@@ -598,7 +621,16 @@ impl Tty {
return;
};
for event in device.drm_scanner.scan_connectors(&device.drm) {
let scan_result = match device.drm_scanner.scan_connectors(&device.drm) {
Ok(x) => x,
Err(err) => {
warn!("error scanning connectors: {err:?}");
return;
}
};
let mut removed = Vec::new();
for event in scan_result {
match event {
DrmScanEvent::Connected {
connector,
@@ -610,11 +642,27 @@ impl Tty {
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
} => self.connector_disconnected(niri, node, crtc),
} => {
self.connector_disconnected(niri, node, crtc);
removed.push(crtc);
}
_ => (),
}
}
// FIXME: this is better done in connector_disconnected(), but currently we call that to
// turn off outputs temporarily, too. So we can't do this there.
let Some(device) = self.devices.get_mut(&node) else {
error!("device disappeared");
return;
};
for crtc in removed {
if device.output_ids.remove(&crtc).is_none() {
error!("output ID missing for disconnected crtc: {crtc:?}");
}
}
self.refresh_ipc_outputs(niri);
}
@@ -696,26 +744,22 @@ impl Tty {
connector: connector::Info,
crtc: crtc::Handle,
) -> anyhow::Result<()> {
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
debug!("connecting connector: {output_name}");
let connector_name = format_connector_name(&connector);
debug!("connecting connector: {connector_name}");
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
.unwrap_or(false);
if non_desktop {
debug!("output is non desktop");
let description = get_edid_info(&device.drm, connector.handle())
.map(|info| truncate_to_nul(info.model))
.unwrap_or_else(|| "Unknown".into());
let description = output_name.format_description();
if let Some(lease_state) = &mut device.drm_lease_state {
lease_state.add_connector::<State>(connector.handle(), output_name, description);
lease_state.add_connector::<State>(connector.handle(), connector_name, description);
}
device
.non_desktop_connectors
@@ -723,12 +767,15 @@ impl Tty {
return Ok(());
}
// This should be unique per CRTC connection, however currently we can call
// connector_connected() multiple times for turning the output off and on.
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
let config = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name.eq_ignore_ascii_case(&output_name))
.find(&output_name)
.cloned()
.unwrap_or_default();
@@ -768,15 +815,13 @@ impl Tty {
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
let word = if config.variable_refresh_rate {
"enabling"
} else {
"disabling"
};
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
if enabled != config.variable_refresh_rate {
if enabled != vrr {
warn!("failed {} VRR", word);
}
@@ -787,13 +832,13 @@ impl Tty {
}
}
} else {
if config.variable_refresh_rate {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate);
let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
@@ -801,7 +846,7 @@ impl Tty {
vrr_enabled = true;
}
}
} else if config.variable_refresh_rate {
} else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
@@ -830,22 +875,13 @@ impl Tty {
// Update the output mode.
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output = Output::new(
output_name.clone(),
connector_name.clone(),
PhysicalProperties {
size: (physical_width as i32, physical_height as i32).into(),
subpixel: connector.subpixel().into(),
model,
make,
model: output_name.model.as_deref().unwrap_or("Unknown").to_owned(),
make: output_name.make.as_deref().unwrap_or("Unknown").to_owned(),
},
);
@@ -856,6 +892,7 @@ impl Tty {
output
.user_data()
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
@@ -880,11 +917,18 @@ impl Tty {
// Filter out the CCS modifiers as they have increased bandwidth, causing some monitor
// configurations to stop working.
let mut render_formats = render_formats.clone();
render_formats.retain(|format| {
!matches!(
format.modifier,
Modifier::I915_y_tiled_ccs
//
// The invalid modifier attempt below should make this unnecessary in some cases, but it
// would still be a bad idea to remove this until Smithay has some kind of full-device
// modesetting test that is able to "downgrade" existing connector modifiers to get enough
// bandwidth for a newly connected one.
let render_formats = render_formats
.iter()
.copied()
.filter(|format| {
!matches!(
format.modifier,
Modifier::I915_y_tiled_ccs
// I915_FORMAT_MOD_Yf_TILED_CCS
| Modifier::Unrecognized(0x100000000000005)
| Modifier::I915_y_tiled_gen12_rc_ccs
@@ -897,23 +941,60 @@ impl Tty {
| Modifier::Unrecognized(0x10000000000000b)
// I915_FORMAT_MOD_4_TILED_DG2_RC_CCS_CC
| Modifier::Unrecognized(0x10000000000000c)
)
});
)
})
.collect::<FormatSet>();
// Create the compositor.
let mut compositor = DrmCompositor::new(
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
allocator,
allocator.clone(),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
// This is only used to pick a good internal format, so it can use the surface's render
// formats, even though we only ever render on the primary GPU.
render_formats.clone(),
device.drm.cursor_size(),
cursor_plane_gbm,
)?;
cursor_plane_gbm.clone(),
);
let mut compositor = match res {
Ok(x) => x,
Err(err) => {
warn!("error creating DRM compositor, will try with invalid modifier: {err:?}");
let render_formats = render_formats
.iter()
.copied()
.filter(|format| format.modifier == Modifier::Invalid)
.collect::<FormatSet>();
// DrmCompositor::new() consumed the surface...
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
let mut planes = surface.planes().clone();
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
allocator,
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
cursor_plane_gbm,
)
.context("error creating DRM compositor")?
}
};
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
@@ -921,7 +1002,7 @@ impl Tty {
let mut dmabuf_feedback = None;
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
let primary_formats = primary_renderer.dmabuf_formats().collect::<HashSet<_>>();
let primary_formats = primary_renderer.dmabuf_formats();
match surface_dmabuf_feedback(
&compositor,
@@ -938,18 +1019,28 @@ impl Tty {
}
}
// Some buggy monitors replug upon powering off, so powering on here would prevent such
// monitors from powering off. Therefore, we avoid unconditionally powering on.
if !niri.monitors_active {
if let Err(err) = compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {output_name}"));
let time_since_presentation_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} time since presentation, ms"));
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
"{connector_name} time since presentation, ms"
));
let presentation_misprediction_plot_name = tracy_client::PlotName::new_leak(format!(
"{output_name} presentation misprediction, ms"
"{connector_name} presentation misprediction, ms"
));
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
tracy_client::PlotName::new_leak(format!("{connector_name} sequence delta"));
let surface = Surface {
name: output_name.clone(),
name: output_name,
connector: connector.handle(),
compositor,
dmabuf_feedback,
gamma_props,
@@ -961,16 +1052,21 @@ impl Tty {
presentation_misprediction_plot_name,
sequence_delta_plot_name,
};
let res = device.surfaces.insert(crtc, surface);
assert!(res.is_none(), "crtc must not have already existed");
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&mut state.backend);
state.niri.queue_redraw(&output);
});
if niri.monitors_active {
// Redraw the new monitor.
niri.event_loop.insert_idle(move |state| {
// Guard against output disconnecting before the idle has a chance to run.
if state.niri.output_state.contains_key(&output) {
state.niri.queue_redraw(&output);
}
});
}
Ok(())
}
@@ -1005,7 +1101,7 @@ impl Tty {
return;
};
debug!("disconnecting connector: {:?}", surface.name);
debug!("disconnecting connector: {:?}", surface.name.connector);
let output = niri
.global_space
@@ -1047,7 +1143,7 @@ impl Tty {
// Finish the Tracy frame, if any.
drop(surface.vblank_frame.take());
let name = &surface.name;
let name = &surface.name.connector;
trace!("vblank on {name} {meta:?}");
span.emit_text(name);
@@ -1105,6 +1201,25 @@ impl Tty {
return;
};
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
state @ (RedrawState::Idle
| RedrawState::Queued
| RedrawState::WaitingForEstimatedVBlank(_)
| RedrawState::WaitingForEstimatedVBlankAndQueued(_)) => {
// This is an error!() because it shouldn't happen, but on some systems it somehow
// does. Kernel sending rogue vblank events?
//
// https://github.com/YaLTeR/niri/issues/556
// https://github.com/YaLTeR/niri/issues/615
error!(
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
can happen when resuming from sleep or powering on monitors: {state:?}"
);
true
}
};
// Mark the last frame as submitted.
match surface.compositor.frame_submitted() {
Ok(Some((mut feedback, target_presentation_time))) => {
@@ -1151,14 +1266,6 @@ impl Tty {
output_state.frame_clock.presented(presentation_time);
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued => unreachable!(),
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
};
if redraw_needed || output_state.unfinished_animations_remain {
let vblank_frame = tracy_client::Client::running()
.unwrap()
@@ -1240,7 +1347,7 @@ impl Tty {
return rv;
};
span.emit_text(&surface.name);
span.emit_text(&surface.name.connector);
if !device.drm.is_active() {
warn!("device is inactive");
@@ -1273,12 +1380,13 @@ impl Tty {
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
.borrow()
.debug
.wait_for_frame_completion_before_queueing
{
let needs_sync = res.needs_sync()
|| self
.config
.borrow()
.debug
.wait_for_frame_completion_before_queueing;
if needs_sync {
if let PrimaryPlaneElement::Swapchain(element) = res.primary_element {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = element.sync.wait() {
@@ -1454,22 +1562,10 @@ impl Tty {
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name.clone());
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1517,9 +1613,10 @@ impl Tty {
.map(logical_output);
let ipc_output = niri_ipc::Output {
name: connector_name.clone(),
make,
model,
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
model: output_name.model.unwrap_or_else(|| "Unknown".into()),
serial: output_name.serial,
physical_size,
modes,
current_mode,
@@ -1528,7 +1625,11 @@ impl Tty {
logical,
};
ipc_outputs.insert(connector_name, ipc_output);
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
error!("output ID missing for crtc: {crtc:?}");
OutputId::next()
});
ipc_outputs.insert(id, ipc_output);
}
}
@@ -1565,10 +1666,36 @@ impl Tty {
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
for surface in device.surfaces.values_mut() {
if let Err(err) = surface.compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
}
}
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
let _span = tracy_client::span!("Tty::set_output_on_demand_vrr");
let output_state = niri.output_state.get_mut(output).unwrap();
output_state.on_demand_vrr_enabled = enable_vrr;
if output_state.frame_clock.vrr() == enable_vrr {
return;
}
for (&node, device) in self.devices.iter_mut() {
for (&crtc, surface) in device.surfaces.iter_mut() {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
if tty_state.node == node && tty_state.crtc == crtc {
try_to_change_vrr(
&device.drm,
surface.connector,
crtc,
surface,
output_state,
enable_vrr,
);
self.refresh_ipc_outputs(niri);
return;
}
}
}
@@ -1588,15 +1715,12 @@ impl Tty {
let mut to_connect = vec![];
for (&node, device) in &mut self.devices {
for surface in device.surfaces.values_mut() {
let crtc = surface.compositor.crtc();
for (&crtc, surface) in device.surfaces.iter_mut() {
let config = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name.eq_ignore_ascii_case(&surface.name))
.find(&surface.name)
.cloned()
.unwrap_or_default();
if config.off {
@@ -1605,12 +1729,8 @@ impl Tty {
}
// Check if we need to change the mode.
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
let Some(connector) = device.drm_scanner.connectors().get(&surface.connector)
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
};
@@ -1621,8 +1741,9 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
let change_vrr = surface.vrr_enabled != config.variable_refresh_rate;
if !change_mode && !change_vrr {
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
let is_on_demand_vrr = config.is_vrr_on_demand();
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1639,37 +1760,21 @@ impl Tty {
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
error!("missing state for output {:?}", surface.name.connector);
continue;
};
if change_vrr {
if is_vrr_capable(&device.drm, connector.handle()) == Some(true) {
let word = if config.variable_refresh_rate {
"enabling"
} else {
"disabling"
};
match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
Ok(enabled) => {
if enabled != config.variable_refresh_rate {
warn!("output {:?}: failed {} VRR", surface.name, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!("output {:?}: error {} VRR: {err:?}", surface.name, word);
}
}
} else if config.variable_refresh_rate {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name
);
}
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
|| (!is_on_demand_vrr && change_always_vrr)
{
try_to_change_vrr(
&device.drm,
connector.handle(),
crtc,
surface,
output_state,
!surface.vrr_enabled,
);
}
if change_mode {
@@ -1678,7 +1783,7 @@ impl Tty {
warn!(
"output {:?}: configured mode {}x{}{} could not be found, \
falling back to preferred",
surface.name,
surface.name.connector,
target.width,
target.height,
if let Some(refresh) = target.refresh {
@@ -1689,7 +1794,10 @@ impl Tty {
);
}
debug!("output {:?}: picking mode: {mode:?}", surface.name);
debug!(
"output {:?}: picking mode: {mode:?}",
surface.name.connector
);
if let Err(err) = surface.compositor.use_mode(mode) {
warn!("error changing mode: {err:?}");
continue;
@@ -1715,18 +1823,13 @@ impl Tty {
continue;
}
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let config = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name.eq_ignore_ascii_case(&output_name))
.find(&output_name)
.cloned()
.unwrap_or_default();
@@ -1765,6 +1868,30 @@ impl Tty {
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
continue;
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
if output_name.matches(target) {
return Some(output_name);
}
}
}
None
}
}
impl GammaProps {
@@ -1870,14 +1997,13 @@ impl GammaProps {
property::Value::Blob(blob).into(),
)
.context("error setting GAMMA_LUT")
.map_err(|err| {
.inspect_err(|_| {
if blob != 0 {
// Destroy the blob we just allocated.
if let Err(err) = device.destroy_property_blob(blob) {
warn!("error destroying GAMMA_LUT property blob: {err:?}");
}
}
err
})?;
}
@@ -1945,20 +2071,20 @@ fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
fn surface_dmabuf_feedback(
compositor: &GbmDrmCompositor,
primary_formats: HashSet<Format>,
primary_formats: FormatSet,
primary_render_node: DrmNode,
surface_render_node: DrmNode,
) -> Result<SurfaceDmabufFeedback, io::Error> {
let surface = compositor.surface();
let planes = surface.planes();
let plane_formats = planes
.primary
let plane_formats = surface
.plane_info()
.formats
.iter()
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
.copied()
.collect::<HashSet<_>>();
.collect::<FormatSet>();
// We limit the scan-out trache to formats we can also render from so that there is always a
// fallback render path available in case the supplied buffer can not be scanned out directly.
@@ -2034,17 +2160,6 @@ fn get_drm_property(
.find_map(|(handle, value)| (handle == prop).then_some(value))
}
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
return;
};
let value = property::Value::Boolean(active);
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
warn!("error setting CRTC property: {err:?}");
}
}
fn refresh_interval(mode: DrmMode) -> Duration {
let clock = mode.clock() as u64;
let htotal = mode.hsync().2 as u64;
@@ -2197,23 +2312,21 @@ fn pick_mode(
mode.map(|m| (*m, fallback))
}
fn truncate_to_nul(mut s: String) -> String {
if let Some(index) = s.find('\0') {
s.truncate(index);
}
s
}
fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<EdidInfo> {
match catch_unwind(AssertUnwindSafe(move || {
EdidInfo::for_connector(device, connector)
})) {
Ok(info) => info,
Err(err) => {
warn!("edid-rs panicked: {err:?}");
None
}
}
fn get_edid_info(
device: &DrmDevice,
connector: connector::Handle,
) -> anyhow::Result<libdisplay_info::info::Info> {
let (_, info, value) =
find_drm_property(device, connector, "EDID").context("no EDID property")?;
let blob = info
.value_type()
.convert_value(value)
.as_blob()
.context("EDID was not blob type")?;
let data = device
.get_property_blob(blob)
.context("error getting EDID blob value")?;
libdisplay_info::info::Info::parse_edid(&data).context("error parsing EDID")
}
fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> anyhow::Result<u64> {
@@ -2319,23 +2432,63 @@ pub fn set_gamma_for_crtc(
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn try_to_change_vrr(
device: &DrmDevice,
connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
enable_vrr: bool,
) {
let _span = tracy_client::span!("try_to_change_vrr");
#[track_caller]
fn check(input: &str, expected: &str) {
let input = String::from(input);
assert_eq!(truncate_to_nul(input), expected);
}
if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
#[test]
fn truncate_to_nul_works() {
check("", "");
check("qwer", "qwer");
check("abc\0def", "abc");
check("\0as", "");
check("a\0\0\0b", "a");
check("bb😁\0cc", "bb😁");
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name.connector
);
}
}
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
)
}
fn make_output_name(
device: &DrmDevice,
connector: connector::Handle,
connector_name: String,
) -> OutputName {
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
OutputName {
connector: connector_name,
make: info.as_ref().and_then(|info| info.make()),
model: info.as_ref().and_then(|info| info.model()),
serial: info.as_ref().and_then(|info| info.serial()),
}
}
+15 -7
View File
@@ -5,7 +5,7 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -15,9 +15,9 @@ 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::reexports::winit::window::Window;
use super::{IpcOutputMap, RenderResult};
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};
@@ -36,11 +36,11 @@ impl Winit {
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
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)?;
let (backend, winit) = winit::init_from_attributes(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -59,13 +59,21 @@ impl Winit {
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
output.user_data().insert_if_missing(|| OutputName {
connector: "winit".to_string(),
make: Some("Smithay".to_string()),
model: Some("Winit".to_string()),
serial: None,
});
let physical_properties = output.physical_properties();
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
OutputId::next(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
serial: None,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
@@ -98,7 +106,7 @@ impl Winit {
{
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
let output = ipc_outputs.get_mut("winit").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;
+14
View File
@@ -13,6 +13,9 @@ use crate::utils::version;
#[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.
@@ -43,6 +46,9 @@ pub enum Sub {
/// 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>,
},
@@ -56,6 +62,12 @@ pub enum Msg {
Outputs,
/// List workspaces.
Workspaces,
/// List open windows.
Windows,
/// Get the configured keyboard layouts.
KeyboardLayouts,
/// Print information about the focused output.
FocusedOutput,
/// Print information about the focused window.
FocusedWindow,
/// Perform an action.
@@ -78,6 +90,8 @@ pub enum Msg {
#[command(subcommand)]
action: OutputAction,
},
/// Start continuously receiving events from the compositor.
EventStream,
/// Print the version of the running niri instance.
Version,
/// Request an error from the running niri instance.
+1 -1
View File
@@ -142,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
}
+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)
}
}
+17 -4
View File
@@ -4,6 +4,7 @@ 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;
@@ -14,6 +15,7 @@ pub mod mutter_screen_cast;
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;
@@ -27,6 +29,7 @@ pub struct DBusServers {
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>,
}
@@ -66,16 +69,26 @@ 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 => (),
}
})
+65 -23
View File
@@ -57,24 +57,20 @@ impl DisplayConfig {
.ipc_outputs
.lock()
.unwrap()
.iter()
.values()
// Take only enabled outputs.
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
.map(|(c, output)| {
.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.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let display_name = make_display_name(output, is_laptop_panel);
let mut properties = HashMap::new();
if is_laptop_panel {
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
);
}
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
@@ -110,8 +106,16 @@ impl DisplayConfig {
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let monitor = Monitor {
names: (c.clone(), String::new(), String::new(), serial),
names: (connector, make, model, serial),
modes,
properties,
};
@@ -143,15 +147,8 @@ impl DisplayConfig {
})
.collect();
// Sort the built-in monitor first, then by connector name.
monitors.sort_unstable_by(|a, b| {
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.0.names.0.cmp(&b.0.names.0))
});
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
@@ -182,3 +179,48 @@ impl Start for DisplayConfig {
Ok(conn)
}
}
// Adapted from Mutter.
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
if is_laptop_panel {
return String::from("Built-in display");
}
let make = &output.make;
let model = &output.model;
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
format_diagonal(diagonal)
}) {
format!("{make} {diagonal}")
} else if model != "Unknown" {
format!("{make} {model}")
} else {
make.clone()
}
}
fn format_diagonal(diagonal_inches: f64) -> String {
let known = [12.1, 13.3, 15.6];
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
format!("{d:.1}")
} else {
format!("{}", diagonal_inches.round() as u32)
}
}
#[cfg(test)]
mod tests {
use k9::snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
snapshot!(format_diagonal(12.11), "12.1″");
snapshot!(format_diagonal(13.28), "13.3″");
snapshot!(format_diagonal(15.6), "15.6″");
snapshot!(format_diagonal(23.2), "23″");
snapshot!(format_diagonal(24.8), "25″");
}
}
+101 -17
View File
@@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
@@ -47,15 +46,40 @@ 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 {
// FIXME: update on scale changes and whatnot.
output: niri_ipc::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 {
@@ -68,14 +92,13 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
output: String,
target: StreamTargetId,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -168,7 +191,11 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.ipc_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()));
};
@@ -176,16 +203,51 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
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.clone(), 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();
@@ -214,10 +276,21 @@ impl Stream {
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let logical = self.output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
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),
}
}
}
}
}
@@ -275,13 +348,13 @@ impl Drop for Session {
}
impl Stream {
pub fn new(
output: niri_ipc::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,
@@ -295,7 +368,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast {
session_id,
output: self.output.name.clone(),
target: self.target.make_id(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
@@ -305,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 },
}
}
}
+4
View File
@@ -40,6 +40,10 @@ impl FrameClock {
self.last_presentation_time = None;
}
pub fn vrr(&self) -> bool {
self.vrr
}
pub fn presented(&mut self, presentation_time: Duration) {
if presentation_time.is_zero() {
// Not interested in these.
+138 -45
View File
@@ -1,14 +1,14 @@
use std::collections::hash_map::Entry;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::input::pointer::CursorImageStatus;
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
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,
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, remove_pre_commit_hook,
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
SurfaceAttributes,
};
@@ -19,6 +19,8 @@ use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
use crate::niri::{ClientState, State};
use crate::utils::send_scale_transform;
use crate::utils::transaction::Transaction;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
@@ -37,52 +39,21 @@ 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);
});
}
}
fn new_surface(&mut self, surface: &WlSurface) {
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
.cached_state
.pending::<SurfaceAttributes>()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
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) {
if let Some(client) = surface.client() {
let res =
state
.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);
}
}
}
}
});
self.add_default_dmabuf_pre_commit_hook(surface);
}
fn commit(&mut self, surface: &WlSurface) {
let _span = tracy_client::span!("CompositorHandler::commit");
trace!(surface = ?surface.id(), "commit");
on_commit_buffer_handler::<Self>(surface);
self.backend.early_import(surface);
@@ -132,7 +103,7 @@ impl CompositorHandler for State {
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
// Chech that the workspace still exists.
// Check that the workspace still exists.
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
@@ -156,6 +127,8 @@ impl CompositorHandler for State {
})
.map(|(mapped, _)| mapped.window.clone());
// The mapped pre-commit hook deals with dma-bufs on its own.
self.remove_default_dmabuf_pre_commit_hook(toplevel.wl_surface());
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
let mapped = Mapped::new(window, rules, hook);
let window = mapped.window.clone();
@@ -209,6 +182,9 @@ impl CompositorHandler for State {
let window = mapped.window.clone();
let output = output.clone();
#[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())
@@ -218,11 +194,13 @@ impl CompositorHandler for State {
});
// Must start the close animation before window.on_commit().
let transaction = Transaction::new();
if !is_mapped {
let blocker = transaction.blocker();
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window);
.start_close_animation_for_window(renderer, &window, blocker);
});
}
@@ -235,7 +213,20 @@ impl CompositorHandler for State {
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);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: id.get(),
});
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface);
// If this is the only instance, then this transaction will complete
// immediately, so no need to set the timer.
if !transaction.is_last() {
transaction.register_deadline_timer(&self.niri.event_loop);
}
if was_active {
self.maybe_warp_cursor_to_focus();
@@ -293,22 +284,68 @@ impl CompositorHandler for State {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(&output.clone());
}
return;
}
// This might be a layer-shell surface.
self.layer_shell_handle_commit(surface);
if self.layer_shell_handle_commit(surface) {
return;
}
// This might be a cursor surface.
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
{
if matches!(
&self.niri.cursor_manager.cursor_image(),
CursorImageStatus::Surface(s) if s == &root_surface
) {
// In case the cursor surface has been committed handle the role specific
// buffer offset by applying the offset on the cursor image hotspot
if surface == &root_surface {
with_states(surface, |states| {
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
if let Some(mut cursor_image_attributes) =
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
{
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
if let Some(buffer_delta) = buffer_delta {
cursor_image_attributes.hotspot -= buffer_delta;
}
}
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a DnD icon surface.
if self.niri.dnd_icon.as_ref() == Some(surface) {
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
// In case the dnd surface has been committed handle the role specific
// buffer offset by applying the offset on the dnd icon offset
if surface == &dnd_icon.surface {
with_states(&dnd_icon.surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take()
.unwrap_or_default();
dnd_icon.offset += buffer_delta;
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a lock surface.
@@ -317,7 +354,7 @@ impl CompositorHandler for State {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
self.niri.queue_redraw(&output.clone());
break;
return;
}
}
}
@@ -346,6 +383,8 @@ impl CompositorHandler for State {
self.niri
.root_surface
.retain(|k, v| k != surface && v != surface);
self.niri.dmabuf_pre_commit_hook.remove(surface);
}
}
@@ -361,3 +400,57 @@ impl ShmHandler for State {
delegate_compositor!(State);
delegate_shm!(State);
impl State {
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
.cached_state
.get::<SurfaceAttributes>()
.pending()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
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) {
if let Some(client) = surface.client() {
let res =
state
.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);
trace!("added default dmabuf blocker");
}
}
}
}
});
let s = surface.clone();
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
error!("tried to add dmabuf pre-commit hook when there was already one");
remove_pre_commit_hook(surface, prev);
}
}
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, hook);
} else {
error!("tried to remove dmabuf pre-commit hook but there was none");
}
}
}
+100 -36
View File
@@ -1,16 +1,18 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
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::{get_parent, with_states};
use smithay::wayland::shell::wlr_layer::{
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
};
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,17 +26,30 @@ 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 wl_surface = surface.wl_surface().clone();
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
assert!(is_new);
let mut map = layer_map_for_output(&output);
map.map_layer(&LayerSurface::new(surface, namespace))
.unwrap();
}
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
let wl_surface = surface.wl_surface();
self.niri.unmapped_layer_surfaces.remove(wl_surface);
let output = if let Some((output, mut map, layer)) =
self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
@@ -61,52 +76,101 @@ impl WlrLayerShellHandler for State {
delegate_layer_shell!(State);
impl State {
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
let Some(output) = self
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
let mut root_surface = surface.clone();
while let Some(parent) = get_parent(&root_surface) {
root_surface = parent;
}
let output = self
.niri
.layout
.outputs()
.find(|o| {
let map = layer_map_for_output(o);
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
.is_some()
})
.cloned()
else {
return;
.cloned();
let Some(output) = output else {
return false;
};
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if surface == &root_surface {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
let mut map = layer_map_for_output(&output);
let mut map = layer_map_for_output(&output);
// Arrange the layers before sending the initial configure to respect any size the
// client may have sent.
map.arrange();
// arrange the layers before sending the initial configure
// to respect any size the client may have sent
map.arrange();
// send the initial configure if relevant
if !initial_configure_sent {
let layer = map
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
.unwrap();
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
});
if initial_configure_sent {
let is_mapped =
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
});
layer.layer_surface().send_configure();
if is_mapped {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
// a big deal since panels generally only open once at the start of the
// session.
//
// Note that:
// 1) Exclusive layer surfaces already get focus automatically in
// update_keyboard_focus().
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand
// surfaces in update_keyboard_focus(), so we don't need to check for that
// here.
//
// https://github.com/YaLTeR/niri/issues/641
let on_demand = layer.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::OnDemand;
if was_unmapped && on_demand {
// I guess it'd make sense to check that no higher-layer on-demand surface
// has focus, but Smithay's Layer doesn't implement Ord so this would be a
// little annoying.
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
self.niri.unmapped_layer_surfaces.insert(surface.clone());
}
} else {
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_scale_transform(surface, data, scale, transform);
});
layer.layer_surface().send_configure();
}
drop(map);
// This will call queue_redraw() inside.
self.niri.output_resized(&output);
} else {
// This is an unsync layer-shell subsurface.
self.niri.queue_redraw(&output);
}
drop(map);
self.niri.output_resized(&output);
true
}
}
+183 -26
View File
@@ -12,26 +12,30 @@ 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::pointer::{
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
};
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::utils::{Logical, Point, Rectangle, Size};
use smithay::wayland::compositor::{get_parent, 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::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
@@ -48,23 +52,33 @@ 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_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
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_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
use crate::niri::{ClientState, State};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, DndIcon, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
use crate::utils::output_size;
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
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_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
};
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
@@ -129,6 +143,59 @@ impl PointerConstraintsHandler for State {
&self.niri.pointer_focus,
);
}
fn cursor_position_hint(
&mut self,
surface: &WlSurface,
pointer: &PointerHandle<Self>,
location: Point<f64, Logical>,
) {
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
constraint.map_or(false, |c| c.is_active())
});
if !is_constraint_active {
return;
}
// Logically the following two checks should always succeed (so, they should print
// error!()s if they fail). However, currently both can fail because niri's pointer focus
// doesn't take pointer grabs into account. So if you start, say, a middle-drag in Blender,
// then touchpad-swipe the window away, the niri pointer focus will change, even though the
// real pointer focus remains on the Blender surface due to the click grab.
//
// FIXME: add error!()s when niri pointer focus takes grabs into account. Alternatively,
// recompute the surface origin here (but that is a bit clunky).
let Some((ref focused_surface, origin)) = self.niri.pointer_focus.surface else {
return;
};
if focused_surface != surface {
return;
}
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
let target = self
.niri
.output_for_root(&root)
.and_then(|output| self.niri.global_space.output_geometry(output))
.map_or(origin + location, |mut output_geometry| {
// i32 sizes are exclusive, but f64 sizes are inclusive.
output_geometry.size -= (1, 1).into();
(origin + location).constrain(output_geometry.to_f64())
});
pointer.set_location(target);
// Redraw to update the cursor position if it's visible.
if !self.niri.pointer_hidden {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
}
}
delegate_pointer_constraints!(State);
@@ -136,11 +203,11 @@ impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
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 = popup.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
}
@@ -213,7 +280,23 @@ impl ClientDndGrabHandler for State {
icon: Option<WlSurface>,
_seat: Seat<Self>,
) {
self.niri.dnd_icon = icon;
let offset = if let CursorImageStatus::Surface(ref surface) =
self.niri.cursor_manager.cursor_image()
{
with_states(surface, |states| {
let hotspot = states
.data_map
.get::<CursorImageSurfaceData>()
.unwrap()
.lock()
.unwrap()
.hotspot;
Point::from((-hotspot.x, -hotspot.y))
})
} else {
(0, 0).into()
};
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
// FIXME: more granular
self.niri.queue_redraw_all();
}
@@ -288,7 +371,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;
};
@@ -303,11 +386,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();
}
@@ -362,6 +445,7 @@ impl ForeignToplevelHandler for State {
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();
}
}
@@ -390,7 +474,7 @@ impl ForeignToplevelHandler for State {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
.move_to_output(Some(&window), &requested_output, None);
}
}
@@ -408,14 +492,30 @@ impl ForeignToplevelHandler for State {
delegate_foreign_toplevel!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, screencopy: Screencopy) {
if let Err(err) = self
.niri
.render_for_screencopy(&mut self.backend, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
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);
@@ -498,3 +598,60 @@ impl GammaControlHandler for State {
}
}
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);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
+334 -94
View File
@@ -1,3 +1,6 @@
use std::cell::Cell;
use calloop::Interest;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -8,33 +11,41 @@ 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};
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::Resource;
use smithay::reexports::wayland_server::{self, Resource, WEnum};
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{
add_pre_commit_hook, send_surface_state, with_states, BufferAssignment, HookId,
SurfaceAttributes,
add_blocker, add_pre_commit_hook, with_states, BufferAssignment, CompositorHandler as _,
HookId, SurfaceAttributes,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::selection::data_device::DnDGrab;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
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,
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use tracing::field::Empty;
use crate::input::move_grab::MoveGrab;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::input::touch_move_grab::TouchMoveGrab;
use crate::input::touch_resize_grab::TouchResizeGrab;
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::{get_monotonic_time, ResizeEdge};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -58,8 +69,96 @@ impl XdgShellHandler for State {
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
let wl_surface = surface.wl_surface();
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
pointer.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Pointer(start_data.clone()));
}
}
}
}
});
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
touch.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab =
grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Touch(start_data.clone()));
}
}
}
}
});
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output_pos = self
.niri
.global_space
.output_geometry(&output)
.unwrap()
.loc
.to_f64();
let pos_within_output = start_data.location() - output_pos;
if !self
.niri
.layout
.interactive_move_begin(window.clone(), &output, pos_within_output)
{
return;
}
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchMoveGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
self.niri.queue_redraw(&output);
}
fn resize_request(
@@ -69,24 +168,39 @@ impl XdgShellHandler for State {
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
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 mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
if pointer.has_grab(serial) {
if let Some(start_data) = pointer.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
}
}
}
}
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
if touch.has_grab(serial) {
if let Some(start_data) = touch.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
}
}
}
}
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
@@ -108,12 +222,12 @@ impl XdgShellHandler for State {
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.layout.reset_window_height();
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.reset_window_height(Some(&window));
}
// FIXME: granular.
self.niri.queue_redraw_all();
@@ -121,14 +235,26 @@ impl XdgShellHandler for State {
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
if !self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchResizeGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
}
fn reposition_request(
@@ -182,10 +308,13 @@ 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.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;
@@ -196,6 +325,7 @@ impl XdgShellHandler for State {
&& 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);
@@ -250,7 +380,7 @@ impl XdgShellHandler for State {
// 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) {
if surface.is_initial_configure_sent() {
surface.send_configure();
}
}
@@ -277,7 +407,7 @@ impl XdgShellHandler for State {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
.move_to_output(Some(&window), &requested_output, None);
}
}
@@ -321,7 +451,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
@@ -405,7 +535,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = workspace_name
@@ -464,19 +594,35 @@ impl XdgShellHandler for State {
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: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let transaction = Transaction::new();
let blocker = transaction.blocker();
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window);
.start_close_animation_for_window(renderer, &window, blocker);
});
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.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
// If this is the only instance, then this transaction will complete immediately, so no
// need to set the timer.
if !transaction.is_last() {
transaction.register_deadline_timer(&self.niri.event_loop);
}
if was_active {
self.maybe_warp_cursor_to_focus();
@@ -525,7 +671,7 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
if toplevel.is_initial_configure_sent() {
toplevel.send_configure();
}
}
@@ -538,17 +684,51 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
if toplevel.is_initial_configure_sent() {
toplevel.send_configure();
}
}
}
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
}
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;
};
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);
@@ -559,18 +739,6 @@ impl XdgForeignHandler for State {
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
})
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
@@ -605,7 +773,12 @@ impl State {
rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|name| {
self.niri
.global_space
.outputs()
.find(|output| output_matches_name(output, name))
})
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
@@ -640,7 +813,7 @@ impl State {
// 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());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
@@ -693,7 +866,7 @@ impl State {
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
workspace_name: ws.and_then(|w| w.name().cloned()),
};
toplevel.send_configure();
@@ -722,22 +895,13 @@ impl State {
if let Some(popup) = self.niri.popups.find_popup(surface) {
match popup {
PopupKind::Xdg(ref popup) => {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<XdgPopupSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if !initial_configure_sent {
if !popup.is_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");
@@ -789,9 +953,9 @@ 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));
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);
target.loc -= get_popup_toplevel_coords(popup).to_f64();
self.position_popup_within_rect(popup, target);
}
@@ -814,10 +978,10 @@ impl State {
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
self.position_popup_within_rect(popup, target);
self.position_popup_within_rect(popup, target.to_f64());
}
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<i32, Logical>) {
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
match popup {
PopupKind::Xdg(popup) => {
popup.with_pending_state(|state| {
@@ -827,28 +991,29 @@ impl State {
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);
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 {
if overflow_x > 0. {
bbox.loc.x -= overflow_x;
}
// Ensure that the popup starts within the window.
bbox.loc.x = bbox.loc.x.max(target.loc.x);
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 += text_input_rectangle.size.h;
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);
popup.set_location(below.loc.to_i32_round());
} else {
popup.set_location(above.loc);
popup.set_location(above.loc.to_i32_round());
}
}
}
@@ -908,25 +1073,25 @@ impl State {
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.
@@ -938,27 +1103,38 @@ 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 span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
let (got_unmapped, commit_serial) = with_states(surface, |states| {
let attrs = states.cached_state.pending::<SurfaceAttributes>();
let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed));
let (got_unmapped, dmabuf, commit_serial) = with_states(surface, |states| {
let (got_unmapped, dmabuf) = {
let mut guard = states.cached_state.get::<SurfaceAttributes>();
match guard.pending().buffer.as_ref() {
Some(BufferAssignment::NewBuffer(buffer)) => {
let dmabuf = get_dmabuf(buffer).cloned().ok();
(false, dmabuf)
}
Some(BufferAssignment::Removed) => (true, None),
None => (false, None),
}
};
let role = states
.data_map
@@ -967,16 +1143,80 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
.lock()
.unwrap();
(got_unmapped, role.configure_serial)
(got_unmapped, dmabuf, role.configure_serial)
});
let animate = if let Some(serial) = commit_serial {
mapped.should_animate_commit(serial)
let mut transaction_for_dmabuf = None;
let mut animate = false;
if let Some(serial) = commit_serial {
if !span.is_disabled() {
span.record("serial", format!("{serial:?}"));
}
trace!("taking pending transaction");
if let Some(transaction) = mapped.take_pending_transaction(serial) {
// Transaction can be already completed if it ran past the deadline.
let disable = state.niri.config.borrow().debug.disable_transactions;
if !transaction.is_completed() && !disable {
// Register the deadline even if this is the last pending, since dmabuf
// rendering can still run over the deadline.
transaction.register_deadline_timer(&state.niri.event_loop);
let is_last = transaction.is_last();
// If this is the last transaction, we don't need to add a separate
// notification, because the transaction will complete in our dmabuf blocker
// callback, which already calls blocker_cleared(), or by the end of this
// function, in which case there would be no blocker in the first place.
if !is_last {
// Waiting for some other surface; register a notification and add a
// transaction blocker.
if let Some(client) = surface.client() {
transaction.add_notification(
state.niri.blocker_cleared_tx.clone(),
client.clone(),
);
add_blocker(surface, transaction.blocker());
}
}
// Delay dropping (and completing) the transaction until the dmabuf is ready.
// If there's no dmabuf, this will be dropped by the end of this pre-commit
// hook.
transaction_for_dmabuf = Some(transaction);
}
}
animate = mapped.should_animate_commit(serial);
} else {
error!("commit on a mapped surface without a configured serial");
false
};
if let Some((blocker, source)) =
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok())
{
if let Some(client) = surface.client() {
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
// This surface is now ready for the transaction.
drop(transaction_for_dmabuf.take());
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);
trace!("added dmabuf blocker");
}
}
}
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
+785 -125
View File
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
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 MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
is_moving: false,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for MoveGrab {
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() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
}
} else {
return;
}
}
// The move 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);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// TODO: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
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);
}
}
+2 -2
View File
@@ -35,7 +35,7 @@ impl PointerGrab<State> for ResizeGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
@@ -60,7 +60,7 @@ impl PointerGrab<State> for ResizeGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
@@ -7,28 +7,45 @@ use smithay::input::pointer::{
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::niri::State;
pub struct ViewOffsetGrab {
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
gesture: GestureState,
}
impl ViewOffsetGrab {
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
#[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 res = state
.niri
.layout
.view_offset_gesture_end(false, Some(false));
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);
}
@@ -41,12 +58,12 @@ impl ViewOffsetGrab {
}
}
impl PointerGrab<State> for ViewOffsetGrab {
impl PointerGrab<State> for SpatialMovementGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
@@ -56,10 +73,34 @@ impl PointerGrab<State> for ViewOffsetGrab {
let delta = event.location - self.last_location;
self.last_location = event.location;
let res = data
.niri
.layout
.view_offset_gesture_update(-delta.x, timestamp, false);
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);
@@ -74,7 +115,7 @@ impl PointerGrab<State> for ViewOffsetGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
+136
View File
@@ -0,0 +1,136 @@
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::niri::State;
pub struct TouchMoveGrab {
start_data: TouchGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
}
impl TouchMoveGrab {
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
}
}
impl TouchGrab<State> for TouchMoveGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
if self.window.alive() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
}
} else {
return;
}
}
// The move is no longer ongoing.
handle.unset_grab(self, data);
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+119
View File
@@ -0,0 +1,119 @@
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::niri::State;
pub struct TouchResizeGrab {
start_data: TouchGrabStartData<State>,
window: Window,
}
impl TouchResizeGrab {
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
Self { start_data, window }
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
}
}
impl TouchGrab<State> for TouchResizeGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
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);
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+250 -108
View File
@@ -1,6 +1,9 @@
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, Socket, Transform,
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
};
use serde_json::json;
@@ -12,18 +15,22 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
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::Windows => Request::Windows,
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
};
let socket = Socket::connect().context("error connecting to the niri socket")?;
let reply = socket
let (reply, mut read_event) = socket
.send(request)
.context("error communicating with niri")?;
@@ -34,6 +41,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Socket::connect()
.and_then(|socket| socket.send(Request::Version))
.ok()
.map(|(reply, _read_event)| reply)
}
_ => None,
};
@@ -113,100 +121,14 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
return Ok(());
}
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let mut outputs = outputs
.into_values()
.map(|out| (OutputName::from_ipc_output(&out), out))
.collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.compare(&b.0));
for (connector, output) in outputs.into_iter() {
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}");
}
for (_name, output) in outputs.into_iter() {
print_output(output)?;
println!();
}
}
@@ -222,23 +144,47 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
}
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)");
}
print_window(&window);
} else {
println!("No window is focused.");
}
}
Msg::Windows => {
let Response::Windows(mut windows) = response else {
bail!("unexpected response: expected Windows, got {response:?}");
};
if json {
let windows =
serde_json::to_string(&windows).context("error formatting response")?;
println!("{windows}");
return Ok(());
}
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
for window in windows {
print_window(&window);
println!();
}
}
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)?;
} else {
println!("No output is focused.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
@@ -309,7 +255,203 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("{is_active}{idx}{name}");
}
}
Msg::KeyboardLayouts => {
let Response::KeyboardLayouts(response) = response else {
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let KeyboardLayouts { names, current_idx } = response;
let current_idx = usize::from(current_idx);
println!("Keyboard layouts:");
for (idx, name) in names.iter().enumerate() {
let is_active = if idx == current_idx { " * " } else { " " };
println!("{is_active}{idx} {name}");
}
}
Msg::EventStream => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
if !json {
println!("Started reading events.");
}
loop {
let event = read_event().context("error reading event from niri")?;
if json {
let event = serde_json::to_string(&event).context("error formatting event")?;
println!("{event}");
continue;
}
match event {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
println!(
"Workspace {workspace_id}: \
active window changed to {active_window_id:?}"
);
}
Event::WindowsChanged { windows } => {
println!("Windows changed: {windows:?}");
}
Event::WindowOpenedOrChanged { window } => {
println!("Window opened or changed: {window:?}");
}
Event::WindowClosed { id } => {
println!("Window closed: {id}");
}
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
}
}
}
}
Ok(())
}
fn print_output(output: Output) -> anyhow::Result<()> {
let Output {
name,
make,
model,
serial,
physical_size,
modes,
current_mode,
vrr_supported,
vrr_enabled,
logical,
} = output;
let serial = serial.as_deref().unwrap_or("Unknown");
println!(r#"Output "{make} {model} {serial}" ({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(())
}
fn print_window(window: &Window) {
let focused = if window.is_focused { " (focused)" } else { "" };
println!("Window ID {}:{focused}", window.id);
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)");
}
if let Some(workspace_id) = window.workspace_id {
println!(" Workspace ID: {workspace_id}");
} else {
println!(" Workspace ID: (none)");
}
}
+427 -33
View File
@@ -1,15 +1,21 @@
use std::cell::RefCell;
use std::collections::HashSet;
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;
use async_channel::{Receiver, Sender, TrySendError};
use calloop::futures::Scheduler;
use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{OutputConfigChanged, Reply, Request, Response};
use smithay::desktop::Window;
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
@@ -17,17 +23,38 @@ use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::backend::IpcOutputMap;
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
use crate::utils::version;
use crate::window::Mapped;
// If an event stream client fails to read events fast enough that we accumulate more than this
// number in our buffer, we drop that event stream client.
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
pub struct IpcServer {
pub socket_path: PathBuf,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
scheduler: Scheduler<()>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
ipc_focused_window: Arc<Mutex<Option<Window>>>,
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
event_stream_state: Rc<RefCell<EventStreamState>>,
}
struct EventStreamClient {
events: Receiver<Event>,
disconnect: Receiver<()>,
write: Box<dyn AsyncWrite + Unpin>,
}
struct EventStreamSender {
events: Sender<Event>,
disconnect: Sender<()>,
}
impl IpcServer {
@@ -59,7 +86,34 @@ impl IpcServer {
})
.unwrap();
Ok(Self { socket_path })
Ok(Self {
socket_path,
event_streams: Rc::new(RefCell::new(Vec::new())),
event_stream_state: Rc::new(RefCell::new(EventStreamState::default())),
})
}
fn send_event(&self, event: Event) {
let mut streams = self.event_streams.borrow_mut();
let mut to_remove = Vec::new();
for (idx, stream) in streams.iter_mut().enumerate() {
match stream.events.try_send(event.clone()) {
Ok(()) => (),
Err(TrySendError::Closed(_)) => to_remove.push(idx),
Err(TrySendError::Full(_)) => {
warn!(
"disconnecting IPC event stream client \
because it is reading events too slowly"
);
to_remove.push(idx);
}
}
}
for idx in to_remove.into_iter().rev() {
let stream = streams.swap_remove(idx);
let _ = stream.disconnect.send_blocking(());
}
}
}
@@ -89,10 +143,14 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
}
};
let ipc_server = state.niri.ipc_server.as_ref().unwrap();
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
scheduler: state.niri.scheduler.clone(),
ipc_outputs: state.backend.ipc_outputs(),
ipc_focused_window: state.niri.ipc_focused_window.clone(),
event_streams: ipc_server.event_streams.clone(),
event_stream_state: ipc_server.event_stream_state.clone(),
};
let future = async move {
@@ -105,7 +163,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
}
}
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
@@ -119,6 +177,7 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let reply = match request {
Ok(request) => process(&ctx, request).await,
@@ -131,9 +190,50 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
}
}
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
}
Ok(())
}
@@ -143,26 +243,29 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Request::Version => Response::Version(version()),
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
Response::Outputs(ipc_outputs)
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
Response::Outputs(outputs.collect())
}
Request::Workspaces => {
let state = ctx.event_stream_state.borrow();
let workspaces = state.workspaces.workspaces.values().cloned().collect();
Response::Workspaces(workspaces)
}
Request::Windows => {
let state = ctx.event_stream_state.borrow();
let windows = state.windows.windows.values().cloned().collect();
Response::Windows(windows)
}
Request::KeyboardLayouts => {
let state = ctx.event_stream_state.borrow();
let layout = state.keyboard_layouts.keyboard_layouts.clone();
let layout = layout.expect("keyboard layouts should be set at startup");
Response::KeyboardLayouts(layout)
}
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(),
}
})
});
let state = ctx.event_stream_state.borrow();
let windows = &state.windows.windows;
let window = windows.values().find(|win| win.is_focused).cloned();
Response::FocusedWindow(window)
}
Request::Action(action) => {
@@ -183,8 +286,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Request::Output { output, action } => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.keys()
.any(|name| name.eq_ignore_ascii_case(&output));
.values()
.any(|o| OutputName::from_ipc_output(o).matches(&output));
let response = if found {
OutputConfigChanged::Applied
} else {
@@ -198,17 +301,308 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Response::OutputConfigChanged(response)
}
Request::Workspaces => {
Request::FocusedOutput => {
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 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 workspaces = result.map_err(|_| String::from("error getting workspace info"))?;
Response::Workspaces(workspaces)
let output = result.map_err(|_| String::from("error getting active output info"))?;
Response::FocusedOutput(output)
}
Request::EventStream => Response::Handled,
};
Ok(response)
}
async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> {
let EventStreamClient {
events,
disconnect,
mut write,
} = client;
while let Ok(event) = events.recv().await {
let mut buf = serde_json::to_vec(&event).context("error formatting event")?;
buf.push(b'\n');
let res = select_biased! {
_ = disconnect.recv().fuse() => return Ok(()),
res = write.write_all(&buf).fuse() => res,
};
match res {
Ok(()) => (),
// Normal client disconnection.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
res @ Err(_) => res.context("error writing event")?,
}
}
Ok(())
}
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
}
})
}
impl State {
pub fn ipc_keyboard_layouts_changed(&mut self) {
let keyboard = self.niri.seat.get_keyboard().unwrap();
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
let layouts = xkb.layouts();
KeyboardLayouts {
names: layouts
.map(|layout| xkb.layout_name(layout).to_owned())
.collect(),
current_idx: xkb.active_layout().0 as u8,
}
});
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.keyboard_layouts;
let event = Event::KeyboardLayoutsChanged { keyboard_layouts };
state.apply(event.clone());
server.send_event(event);
}
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
let keyboard = self.niri.seat.get_keyboard().unwrap();
let idx = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
xkb.active_layout().0 as u8
});
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.keyboard_layouts;
if state.keyboard_layouts.as_ref().unwrap().current_idx == idx {
return;
}
let event = Event::KeyboardLayoutSwitched { idx };
state.apply(event.clone());
server.send_event(event);
}
pub fn ipc_refresh_layout(&mut self) {
self.ipc_refresh_workspaces();
self.ipc_refresh_windows();
}
fn ipc_refresh_workspaces(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let _span = tracy_client::span!("State::ipc_refresh_workspaces");
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.workspaces;
let mut events = Vec::new();
let layout = &self.niri.layout;
let focused_ws_id = layout.active_workspace().map(|ws| ws.id().get());
// Check for workspace changes.
let mut seen = HashSet::new();
let mut need_workspaces_changed = false;
for (mon, ws_idx, ws) in layout.workspaces() {
let id = ws.id().get();
seen.insert(id);
let Some(ipc_ws) = state.workspaces.get(&id) else {
// A new workspace was added.
need_workspaces_changed = true;
break;
};
// Check for any changes that we can't signal as individual events.
let output_name = mon.map(|mon| mon.output_name());
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|| ipc_ws.name.as_ref() != ws.name()
|| ipc_ws.output.as_ref() != output_name
{
need_workspaces_changed = true;
break;
}
let active_window_id = ws.active_window().map(|win| win.id().get());
if ipc_ws.active_window_id != active_window_id {
events.push(Event::WorkspaceActiveWindowChanged {
workspace_id: id,
active_window_id,
});
}
// Check if this workspace became focused.
let is_focused = Some(id) == focused_ws_id;
if is_focused && !ipc_ws.is_focused {
events.push(Event::WorkspaceActivated { id, focused: true });
continue;
}
// Check if this workspace became active.
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
if is_active && !ipc_ws.is_active {
events.push(Event::WorkspaceActivated { id, focused: false });
}
}
// Check if any workspaces were removed.
if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) {
need_workspaces_changed = true;
}
if need_workspaces_changed {
events.clear();
let workspaces = layout
.workspaces()
.map(|(mon, ws_idx, ws)| {
let id = ws.id().get();
Workspace {
id,
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name().cloned(),
output: mon.map(|mon| mon.output_name().clone()),
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
is_focused: Some(id) == focused_ws_id,
active_window_id: ws.active_window().map(|win| win.id().get()),
}
})
.collect();
events.push(Event::WorkspacesChanged { workspaces });
}
for event in events {
state.apply(event.clone());
server.send_event(event);
}
}
fn ipc_refresh_windows(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let _span = tracy_client::span!("State::ipc_refresh_windows");
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.windows;
let mut events = Vec::new();
let layout = &self.niri.layout;
// Check for window changes.
let mut seen = HashSet::new();
let mut focused_id = None;
layout.with_windows(|mapped, _, ws_id| {
let id = mapped.id().get();
seen.insert(id);
if mapped.is_focused() {
focused_id = Some(id);
}
let Some(ipc_win) = state.windows.get(&id) else {
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
};
let workspace_id = ws_id.map(|id| id.get());
let mut changed = ipc_win.workspace_id != workspace_id;
let wl_surface = mapped.toplevel().wl_surface();
changed |= with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
ipc_win.title != role.title || ipc_win.app_id != role.app_id
});
if changed {
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
}
if mapped.is_focused() && !ipc_win.is_focused {
events.push(Event::WindowFocusChanged { id: Some(id) });
}
});
// Check for closed windows.
let mut ipc_focused_id = None;
for (id, ipc_win) in &state.windows {
if !seen.contains(id) {
events.push(Event::WindowClosed { id: *id });
}
if ipc_win.is_focused {
ipc_focused_id = Some(id);
}
}
// Extra check for focus becoming None, since the checks above only work for focus becoming
// a different window.
if focused_id.is_none() && ipc_focused_id.is_some() {
events.push(Event::WindowFocusChanged { id: None });
}
for event in events {
state.apply(event.clone());
server.send_event(event);
}
}
}
+121 -53
View File
@@ -5,14 +5,14 @@ use anyhow::Context as _;
use glam::{Mat3, Vec2};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Id, Kind, RenderElement};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::{Renderer as _, Texture};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{Blocker, BlockerState};
use crate::animation::Animation;
use crate::niri_render_elements;
@@ -20,39 +20,35 @@ 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};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
pub struct ClosingWindow {
/// Contents of the window.
texture: GlesTexture,
buffer: TextureBuffer<GlesTexture>,
/// Blocked-out contents of the window.
blocked_out_texture: GlesTexture,
/// Scale that the textures was rendered with.
texture_scale: Scale<f64>,
/// ID of the textures' renderer.
texture_renderer_id: usize,
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<i32, Logical>,
geo_size: Size<f64, Logical>,
/// Position in the workspace.
pos: Point<i32, Logical>,
pos: Point<f64, Logical>,
/// How much the texture should be offset.
texture_offset: Point<f64, Logical>,
buffer_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_texture_offset: Point<f64, Logical>,
blocked_out_buffer_offset: Point<f64, Logical>,
/// The closing animation.
anim: Animation,
anim_state: AnimationState,
/// Random seed for the shader.
random_seed: f32,
@@ -65,13 +61,37 @@ niri_render_elements! {
}
}
#[derive(Debug)]
enum AnimationState {
Waiting {
/// Blocker for a transaction before starting the animation.
blocker: TransactionBlocker,
anim: Animation,
},
Animating(Animation),
}
impl AnimationState {
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
if blocker.state() == BlockerState::Pending {
Self::Waiting { blocker, anim }
} else {
// This actually doesn't normally happen because the window is removed only after the
// closing animation is created. Though, it does happen with disable-transactions debug
// flag.
Self::Animating(anim)
}
}
}
impl ClosingWindow {
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: Scale<f64>,
geo_size: Size<i32, Logical>,
pos: Point<i32, Logical>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
blocker: TransactionBlocker,
anim: Animation,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
@@ -86,69 +106,121 @@ impl ClosingWindow {
)
.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((texture, offset))
Ok((buffer, offset))
};
let (texture, texture_offset) =
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (blocked_out_texture, blocked_out_texture_offset) =
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
texture,
blocked_out_texture,
texture_scale: scale,
texture_renderer_id: renderer.id(),
buffer,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
texture_offset,
blocked_out_texture_offset,
anim,
buffer_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
})
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let mut anim = anim.restarted(0., 1., 0.);
anim.set_current_time(current_time);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(anim) => anim.set_current_time(current_time),
}
}
pub fn are_animations_ongoing(&self) -> bool {
!self.anim.is_done()
match &self.anim_state {
AnimationState::Waiting { .. } => true,
AnimationState::Animating(anim) => !anim.is_done(),
}
}
pub fn render(
&self,
renderer: &mut GlesRenderer,
view_rect: Rectangle<i32, Logical>,
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 (texture, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_texture, self.blocked_out_texture_offset)
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.texture, self.texture_offset)
(&self.buffer, self.buffer_offset)
};
let anim = match &self.anim_state {
AnimationState::Waiting { .. } => {
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
1.,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), 1.);
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,
);
return elem.into();
}
AnimationState::Animating(anim) => anim,
};
let progress = anim.value();
let clamped_progress = anim.clamped_value().clamp(0., 1.);
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);
let geo_loc = Vec2::new(self.pos.x as f32, self.pos.y 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 = Vec2::new(self.texture_scale.x as f32, self.texture_scale.y as f32);
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 = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale;
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);
@@ -157,6 +229,7 @@ impl ClosingWindow {
ProgramType::Close,
view_rect.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
@@ -166,22 +239,17 @@ impl ClosingWindow {
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())]),
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
Kind::Unspecified,
)
.with_location(Point::from((0, 0)))
.with_location(Point::from((0., 0.)))
.into();
}
let elem = TextureRenderElement::from_static_texture(
Id::new(),
self.texture_renderer_id,
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
texture.clone(),
self.texture_scale.x as i32,
Transform::Normal,
Some(1. - clamped_progress as f32),
None,
1. - clamped_progress as f32,
None,
None,
Kind::Unspecified,
@@ -189,15 +257,15 @@ impl ClosingWindow {
let elem = PrimaryGpuTextureRenderElement(elem);
let center = self.geo_size.to_point().to_f64().downscale(2.);
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.to_f64() + offset;
location.x -= view_rect.loc.x as f64;
let mut location = self.pos + offset;
location.x -= view_rect.loc.x;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
+48 -42
View File
@@ -1,23 +1,22 @@
use std::cmp::{max, min};
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
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, Rectangle, 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; 8],
locations: [Point<i32, Logical>; 8],
sizes: [Size<i32, Logical>; 8],
locations: [Point<f64, Logical>; 8],
sizes: [Size<f64, Logical>; 8],
borders: [BorderRenderElement; 8],
full_size: Size<i32, Logical>,
full_size: Size<f64, Logical>,
is_border: bool,
use_border_shader: bool,
config: niri_config::FocusRing,
@@ -56,14 +55,15 @@ impl FocusRing {
pub fn update_render_elements(
&mut self,
win_size: Size<i32, Logical>,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
view_rect: Rectangle<i32, Logical>,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
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
@@ -72,7 +72,7 @@ impl FocusRing {
};
for buf in &mut self.buffers {
buf.set_color(color.into());
buf.set_color(color.to_array_premul());
}
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
@@ -91,6 +91,7 @@ impl FocusRing {
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
@@ -107,39 +108,48 @@ impl FocusRing {
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 = max(width, radius.top_left.ceil() as i32);
let top_right = min(
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
let top_right = f64::min(
self.full_size.w - top_left,
max(width, radius.top_right.ceil() as i32),
f64::max(width, ceil(f64::from(radius.top_right))),
);
let bottom_left = min(
let bottom_left = f64::min(
self.full_size.h - top_left,
max(width, radius.bottom_left.ceil() as i32),
f64::max(width, ceil(f64::from(radius.bottom_left))),
);
let bottom_right = min(
let bottom_right = f64::min(
self.full_size.h - top_right,
min(
f64::min(
self.full_size.w - bottom_left,
max(width, radius.bottom_right.ceil() as i32),
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.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));
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.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.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.
@@ -169,12 +179,14 @@ impl FocusRing {
border.update(
size,
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
gradient.from.into(),
gradient.to.into(),
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 {
@@ -188,12 +200,14 @@ impl FocusRing {
gradient_area.loc - self.locations[0],
gradient_area.size,
),
gradient.from.into(),
gradient.to.into(),
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,
);
}
@@ -203,8 +217,7 @@ impl FocusRing {
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 8>::new();
@@ -215,24 +228,17 @@ impl FocusRing {
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 {
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<i32, Logical>| {
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.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
};
rv.push(elem);
};
@@ -252,8 +258,8 @@ impl FocusRing {
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.config.width.into()
pub fn width(&self) -> f64 {
self.config.width.0
}
pub fn is_off(&self) -> bool {
+2265 -334
View File
File diff suppressed because it is too large Load Diff
+370 -253
View File
@@ -7,8 +7,9 @@ use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use smithay::utils::{Logical, Point, Rectangle};
use super::tile::Tile;
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
@@ -19,7 +20,8 @@ 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, ResizeEdge};
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, round_logical_in_physical, ResizeEdge};
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
@@ -32,17 +34,19 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
pub output: Output,
pub(super) output: Output,
/// Cached name of the output.
output_name: String,
// Must always contain at least one.
pub workspaces: Vec<Workspace<W>>,
pub(super) workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
pub(super) active_workspace_idx: usize,
/// ID of the previously active workspace.
pub previous_workspace_id: Option<WorkspaceId>,
pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
pub(super) workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
pub options: Rc<Options>,
pub(super) options: Rc<Options>,
}
#[derive(Debug)]
@@ -54,10 +58,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,
pub tracker: SwipeTracker,
pub(super) current_idx: f64,
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
}
pub type MonitorRenderElement<R> =
@@ -90,6 +96,7 @@ impl WorkspaceSwitch {
impl<W: LayoutElement> Monitor<W> {
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
Self {
output_name: output.name(),
output,
workspaces,
active_workspace_idx: 0,
@@ -99,6 +106,18 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn output(&self) -> &Output {
&self.output
}
pub fn output_name(&self) -> &String {
&self.output_name
}
pub fn active_workspace_idx(&self) -> usize {
self.active_workspace_idx
}
pub fn active_workspace_ref(&self) -> &Workspace<W> {
&self.workspaces[self.active_workspace_idx]
}
@@ -123,6 +142,14 @@ impl<W: LayoutElement> Monitor<W> {
&mut self.workspaces[self.active_workspace_idx]
}
pub fn windows(&self) -> impl Iterator<Item = &W> {
self.workspaces.iter().flat_map(|ws| ws.windows())
}
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
@@ -157,7 +184,7 @@ impl<W: LayoutElement> Monitor<W> {
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window(window, activate, width, is_full_width);
workspace.add_window(None, window, activate, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
@@ -191,12 +218,15 @@ impl<W: LayoutElement> Monitor<W> {
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
// Since we're adding window right of something, the workspace isn't empty, and therefore
// cannot be the last one, so we never need to insert a new empty workspace.
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_column(column, activate);
workspace.add_column(None, column, activate, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
@@ -212,6 +242,56 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn add_tile(
&mut self,
workspace_idx: usize,
column_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn add_tile_to_column(
&mut self,
workspace_idx: usize,
column_idx: usize,
tile_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile_to_column(column_idx, tile_idx, tile, activate);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
// Since we're adding window to an existing column, the workspace isn't empty, and
// therefore cannot be the last one, so we never need to insert a new empty workspace.
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
@@ -296,14 +376,6 @@ 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();
}
@@ -320,6 +392,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();
}
@@ -328,6 +408,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_right();
} else {
workspace.focus_up();
}
}
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
@@ -373,13 +509,20 @@ 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_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
.into_window();
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(new_idx, window, true, width, is_full_width);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
removed.width,
removed.is_full_width,
);
}
pub fn move_to_workspace_down(&mut self) {
@@ -396,17 +539,48 @@ 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_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
.into_window();
let removed = workspace.remove_tile_by_idx(
workspace.active_column_idx,
column.active_tile_idx,
Transaction::new(),
None,
);
self.add_window(new_idx, window, true, width, is_full_width);
self.add_window(
new_idx,
removed.tile.into_window(),
true,
removed.width,
removed.is_full_width,
);
}
pub fn move_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window {
self.workspaces
.iter()
.enumerate()
.find_map(|(ws_idx, ws)| {
ws.columns.iter().enumerate().find_map(|(col_idx, col)| {
col.tiles
.iter()
.position(|tile| tile.window().id() == window)
.map(|tile_idx| (ws_idx, col_idx, tile_idx))
})
})
.unwrap()
} else {
let ws_idx = self.active_workspace_idx;
let ws = &self.workspaces[ws_idx];
if ws.columns.is_empty() {
return;
}
let col_idx = ws.active_column_idx;
let tile_idx = ws.columns[col_idx].active_tile_idx;
(ws_idx, col_idx, tile_idx)
};
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
@@ -414,23 +588,24 @@ impl<W: LayoutElement> Monitor<W> {
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
let column = &workspace.columns[col_idx];
let activate = source_workspace_idx == self.active_workspace_idx
&& col_idx == workspace.active_column_idx
&& tile_idx == column.active_tile_idx;
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
self.add_window(
new_idx,
removed.tile.into_window(),
activate,
removed.width,
removed.is_full_width,
);
if self.workspace_switch.is_none() {
self.clean_up_workspaces();
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
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);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn move_column_to_workspace_up(&mut self) {
@@ -446,7 +621,7 @@ impl<W: LayoutElement> Monitor<W> {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
}
@@ -463,7 +638,7 @@ impl<W: LayoutElement> Monitor<W> {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
}
@@ -480,13 +655,8 @@ impl<W: LayoutElement> Monitor<W> {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
self.add_column(new_idx, column, true);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn switch_workspace_up(&mut self) {
@@ -507,10 +677,6 @@ 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;
self.clean_up_workspaces();
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
@@ -567,7 +733,7 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn are_animations_ongoing(&self) -> bool {
pub(super) fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())
@@ -617,11 +783,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);
}
}
@@ -640,14 +808,6 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().set_column_width(change);
}
pub fn set_window_height(&mut self, change: SizeChange) {
self.active_workspace().set_window_height(change);
}
pub fn 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 {
@@ -695,106 +855,91 @@ impl<W: LayoutElement> Monitor<W> {
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
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);
let size = output_size(&self.output).to_f64();
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = (offset * size.h as f64).round() as i32;
let offset = offset * size.h;
let clip_rect = Rectangle::from_loc_and_size((0, -offset), size);
let clip_rect = Rectangle::from_loc_and_size((0., -offset), size);
rect = rect.intersection(clip_rect)?;
}
Some(rect)
}
pub fn workspaces_with_render_positions(
&self,
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
let mut first = None;
let mut second = None;
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 < self.workspaces.len() as f64 {
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
let offset =
round_logical_in_physical(scale, (render_idx - before_idx) * size.h);
// Ceil the height in physical pixels.
let height = (size.h * scale).ceil() / scale;
if before_idx >= 0. {
let before_idx = before_idx as usize;
let before_offset = Point::from((0., -offset));
first = Some((&self.workspaces[before_idx], before_offset));
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
let after_offset = Point::from((0., -offset + height));
second = Some((&self.workspaces[after_idx], after_offset));
}
}
}
None => {
first = Some((
&self.workspaces[self.active_workspace_idx],
Point::from((0., 0.)),
));
}
}
first.into_iter().chain(second)
}
pub fn workspace_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&Workspace<W>, Point<f64, Logical>)> {
let size = output_size(&self.output);
let (ws, bounds) = self
.workspaces_with_render_positions()
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
Some((ws, bounds.loc))
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
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];
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
Some((win, win_pos.map(|p| p - ws_offset)))
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.window_under(pos_within_output)
}
}
) -> Option<(&W, Option<Point<f64, Logical>>)> {
let (ws, offset) = self.workspace_under(pos_within_output)?;
let (win, win_pos) = ws.window_under(pos_within_output - offset)?;
Some((win, win_pos.map(|p| p + offset)))
}
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 as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
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.to_f64())
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.resize_edges_under(pos_within_output)
}
}
let (ws, offset) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - offset)
}
pub fn render_above_top_layer(&self) -> bool {
@@ -807,108 +952,55 @@ impl<W: LayoutElement> Monitor<W> {
ws.render_above_top_layer()
}
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
pub fn render_elements<'a, R: NiriRenderer>(
&'a self,
renderer: &'a mut R,
target: RenderTarget,
) -> Vec<MonitorRenderElement<R>> {
) -> impl Iterator<Item = 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);
// Ceil the height in physical pixels.
let height = (size.h * scale).ceil() as i32;
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
// Crop the elements to prevent them overflowing, currently visible during a workspace
// switch.
//
// HACK: crop to infinite bounds at least horizontally where we
// know there's no workspace joining or monitor bounds, otherwise
// it will cut pixel shaders and mess up the coordinate space.
// There's also a damage tracking bug which causes glitched
// rendering for maximized GTK windows.
//
// FIXME: use proper bounds after fixing the Crop element.
let crop_bounds = if self.workspace_switch.is_some() {
Rectangle::from_loc_and_size((-i32::MAX / 2, 0), (i32::MAX, height))
} else {
Rectangle::from_loc_and_size((-i32::MAX / 2, -i32::MAX / 2), (i32::MAX, i32::MAX))
};
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return vec![];
}
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
let after = self.workspaces[after_idx].render_elements(renderer, target);
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: crop to infinite bounds for all sides except the side
// where the workspaces join,
// otherwise it will cut pixel shaders and mess up
// the coordinate space.
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
if before_idx < 0. {
return after.collect();
}
Some(after)
} else {
None
};
let before_idx = before_idx as usize;
let before = self.workspaces[before_idx].render_elements(renderer, target);
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, size.h),
),
)?,
(0, -offset),
Relocate::Relative,
))
});
before.chain(after.into_iter().flatten()).collect()
}
None => {
let elements =
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
elements
self.workspaces_with_render_positions()
.flat_map(move |(ws, offset)| {
ws.render_elements(renderer, target)
.into_iter()
.filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: set infinite crop bounds due to a damage tracking bug
// which causes glitched rendering for maximized GTK windows.
// FIXME: use proper bounds after fixing the Crop element.
Rectangle::from_loc_and_size(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX, i32::MAX),
),
// Rectangle::from_loc_and_size((0, 0), size),
)?,
(0, 0),
Relocate::Relative,
))
.filter_map(move |elem| {
CropRenderElement::from_element(elem, scale, crop_bounds)
})
.collect()
}
}
.map(move |elem| {
RelocateRenderElement::from_element(
elem,
// The offset we get from workspaces_with_render_positions() is already
// rounded to physical pixels, but it's in the logical coordinate
// space, so we need to convert it to physical.
offset.to_physical_precise_round(scale),
Relocate::Relative,
)
})
})
}
pub fn workspace_switch_gesture_begin(&mut self) {
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
let center_idx = self.active_workspace_idx;
let current_idx = self
.workspace_switch
@@ -920,6 +1012,7 @@ impl<W: LayoutElement> Monitor<W> {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
is_touchpad,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
@@ -928,14 +1021,24 @@ impl<W: LayoutElement> Monitor<W> {
&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 pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
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;
@@ -950,20 +1053,34 @@ impl<W: LayoutElement> Monitor<W> {
Some(true)
}
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
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 mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
let 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;
+18 -20
View File
@@ -4,13 +4,12 @@ use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Id, Kind, RenderElement};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::{Renderer as _, Texture};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
@@ -19,6 +18,7 @@ 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 {
@@ -55,8 +55,8 @@ impl OpenAnimation {
&self,
renderer: &mut GlesRenderer,
elements: &[impl RenderElement<GlesRenderer>],
geo_size: Size<i32, Logical>,
location: Point<i32, Logical>,
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
let progress = self.anim.value();
@@ -75,17 +75,17 @@ impl OpenAnimation {
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.to_f64() + offset, texture_size);
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();
area.loc -= diff.downscale(2.);
area.size += diff.to_size();
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 = area.to_i32_up();
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);
@@ -106,6 +106,7 @@ impl OpenAnimation {
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
@@ -122,15 +123,12 @@ impl OpenAnimation {
.into());
}
let elem = TextureRenderElement::from_static_texture(
Id::new(),
renderer.id(),
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
texture.clone(),
scale.x as i32,
Transform::Normal,
Some(clamped_progress as f32),
None,
clamped_progress as f32,
None,
None,
Kind::Unspecified,
@@ -138,7 +136,7 @@ impl OpenAnimation {
let elem = PrimaryGpuTextureRenderElement(elem);
let center = geo_size.to_point().to_f64().downscale(2.);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
@@ -147,7 +145,7 @@ impl OpenAnimation {
let elem = RelocateRenderElement::from_element(
elem,
(location.to_f64() + offset).to_physical_precise_round(scale),
(location + offset).to_physical_precise_round(scale),
Relocate::Relative,
);
+148 -103
View File
@@ -1,10 +1,8 @@
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use niri_config::CornerRadius;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
@@ -23,7 +21,9 @@ 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};
use crate::utils::transaction::Transaction;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -50,7 +50,7 @@ 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>,
@@ -64,14 +64,20 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
/// 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.
pub options: Rc<Options>,
pub(super) options: Rc<Options>,
}
niri_render_elements! {
@@ -87,24 +93,24 @@ niri_render_elements! {
}
}
type TileRenderSnapshot =
pub type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
struct ResizeAnimation {
anim: Animation,
size_from: Size<i32, Logical>,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
}
#[derive(Debug)]
struct MoveAnimation {
anim: Animation,
from: i32,
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());
@@ -114,19 +120,22 @@ impl<W: LayoutElement> Tile<W> {
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,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
scale,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
self.scale = scale;
self.options = options;
let rules = self.window.rules();
@@ -147,7 +156,7 @@ impl<W: LayoutElement> Tile<W> {
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();
}
@@ -160,16 +169,16 @@ impl<W: LayoutElement> Tile<W> {
let val = resize.anim.value();
let size_from = resize.size_from;
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
size.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_point() - size_from.to_point();
let change = max(change.x.abs(), change.y.abs());
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 {
@@ -235,13 +244,13 @@ impl<W: LayoutElement> Tile<W> {
|| self.move_y_animation.is_some()
}
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<i32, Logical>) {
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 border_width = self.effective_border_width().unwrap_or(0.);
let radius = if self.is_fullscreen {
CornerRadius::default()
} else {
@@ -260,6 +269,7 @@ impl<W: LayoutElement> Tile<W> {
view_rect.size,
),
radius,
self.scale,
);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@@ -281,20 +291,27 @@ impl<W: LayoutElement> Tile<W> {
!draw_focus_ring_with_background,
view_rect,
radius,
self.scale,
);
}
pub fn render_offset(&self) -> Point<i32, Logical> {
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 += f64::from(move_.from) * move_.anim.value();
offset.x += move_.from * move_.anim.value();
}
if let Some(move_) = &self.move_y_animation {
offset.y += f64::from(move_.from) * move_.anim.value();
offset.y += move_.from * move_.anim.value();
}
offset.to_i32_round()
offset += self.interactive_move_offset;
offset
}
pub fn start_open_animation(&mut self) {
@@ -310,16 +327,16 @@ impl<W: LayoutElement> Tile<W> {
self.resize_animation.as_ref().map(|resize| &resize.anim)
}
pub fn animate_move_from(&mut self, from: Point<i32, Logical>) {
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: i32) {
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: i32, config: niri_config::Animation) {
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.
@@ -334,11 +351,11 @@ impl<W: LayoutElement> Tile<W> {
});
}
pub fn animate_move_y_from(&mut self, from: i32) {
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: i32, config: niri_config::Animation) {
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.
@@ -353,6 +370,11 @@ impl<W: LayoutElement> Tile<W> {
});
}
pub fn stop_move_animations(&mut self) {
self.move_x_animation = None;
self.move_y_animation = None;
}
pub fn window(&self) -> &W {
&self.window
}
@@ -370,7 +392,7 @@ impl<W: LayoutElement> Tile<W> {
}
/// 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;
}
@@ -383,22 +405,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() {
@@ -408,68 +435,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
}
fn animated_window_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
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;
let size_from = resize.size_from.to_f64();
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
size.w = max(1, size.w);
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
size.h = max(1, size.h);
size.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<i32, Logical> {
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 = 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 buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
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
}
@@ -479,74 +511,91 @@ 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>, animate: bool) {
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
animate: bool,
transaction: Option<Transaction>,
) {
// 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, animate);
// 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, transaction);
}
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.;
}
}
@@ -567,7 +616,7 @@ impl<W: LayoutElement> Tile<W> {
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
@@ -581,7 +630,7 @@ impl<W: LayoutElement> Tile<W> {
};
let window_loc = self.window_loc();
let window_size = self.window_size();
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);
@@ -609,7 +658,7 @@ impl<W: LayoutElement> Tile<W> {
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)),
Point::from((0., 0.)),
scale,
1.,
target,
@@ -664,8 +713,7 @@ impl<W: LayoutElement> Tile<W> {
resize_fallback = Some(
SolidColorRenderElement::from_buffer(
&fallback_buffer,
area.loc.to_physical_precise_round(scale),
scale,
area.loc,
alpha,
Kind::Unspecified,
)
@@ -726,13 +774,15 @@ impl<W: LayoutElement> Tile<W> {
if radius != CornerRadius::default() && has_border_shader {
return BorderRenderElement::new(
geo.size,
Rectangle::from_loc_and_size((0, 0), geo.size),
elem.color(),
elem.color(),
Rectangle::from_loc_and_size((0., 0.), geo.size),
GradientInterpolation::default(),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_loc_and_size((0, 0), geo.size),
Rectangle::from_loc_and_size((0., 0.), geo.size),
0.,
radius,
scale.x as f32,
)
.with_location(geo.loc)
.into();
@@ -758,8 +808,7 @@ impl<W: LayoutElement> Tile<W> {
let elem = self.is_fullscreen.then(|| {
SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
location,
1.,
Kind::Unspecified,
)
@@ -769,23 +818,19 @@ impl<W: LayoutElement> Tile<W> {
let elem = self.effective_border_width().map(|width| {
self.border
.render(renderer, location + Point::from((width, width)), scale)
.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, scale)
.map(Into::into)
});
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<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
@@ -798,7 +843,7 @@ impl<W: LayoutElement> Tile<W> {
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);
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) => {
@@ -843,7 +888,7 @@ impl<W: LayoutElement> Tile<W> {
let contents = self.render(
renderer,
Point::from((0, 0)),
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
@@ -852,7 +897,7 @@ impl<W: LayoutElement> Tile<W> {
// 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)),
Point::from((0., 0.)),
scale,
false,
RenderTarget::Screencast,
+1709 -886
View File
File diff suppressed because it is too large Load Diff
+91 -60
View File
@@ -18,11 +18,13 @@ use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
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 niri_ipc::socket::SOCKET_PATH_ENV;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::EventLoop;
@@ -89,9 +91,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Sub::Validate { config } => {
tracy_client::Client::start();
let path = config
.or_else(default_config_path)
.expect("error getting config path");
let (path, _, _) = config_path(config);
Config::load(&path)?;
info!("config is valid");
return Ok(());
@@ -112,54 +112,50 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load the config.
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
let (path, watch_path, create_default) = config_path(cli.config);
env::remove_var("NIRI_CONFIG");
if create_default {
let default_parent = 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)
match fs::create_dir_all(default_parent) {
Ok(()) => {
// 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(&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 {:?}", &path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &path),
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
Err(err) => {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
}
}
Some(default_path)
});
}
let mut config_errored = false;
let mut config = path
.as_deref()
.and_then(|path| match Config::load(path) {
Ok(config) => Some(config),
Err(err) => {
warn!("{err:?}");
config_errored = true;
None
}
let mut config = Config::load(&path)
.map_err(|err| {
warn!("{err:?}");
config_errored = true;
})
.unwrap_or_default();
@@ -173,6 +169,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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();
let display = Display::new().unwrap();
@@ -194,7 +192,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
@@ -214,30 +212,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "dbus")]
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:?}");
};
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
// 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:?}");
// 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.clone() {
let _watcher = {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
let watcher = Watcher::new(watch_path.clone(), tx);
event_loop
.handle()
.insert_source(rx, move |event, _, state| match event {
calloop::channel::Event::Msg(()) => state.reload_config(path.clone()),
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
calloop::channel::Event::Closed => (),
})
.unwrap();
Some(watcher)
} else {
None
watcher
};
// Spawn commands from cli and auto-start.
@@ -267,7 +265,7 @@ fn import_environment() {
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
SOCKET_PATH_ENV,
]
.join(" ");
@@ -312,6 +310,12 @@ fn import_environment() {
}
}
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");
@@ -323,6 +327,33 @@ fn default_config_path() -> Option<PathBuf> {
Some(path)
}
fn system_config_path() -> PathBuf {
PathBuf::from("/etc/niri/config.kdl")
}
/// Resolves and returns the config path to load, the config path to watch, and whether to create
/// the default config at the path to load.
fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
if let Some(explicit) = cli_path.or_else(env_config_path) {
return (explicit.clone(), explicit, false);
}
let system_path = system_config_path();
if let Some(path) = default_config_path() {
if path.exists() {
return (path.clone(), path, true);
}
if system_path.exists() {
(system_path, path, false)
} else {
(path.clone(), path, true)
}
} else {
(system_path.clone(), system_path, false)
}
}
fn notify_fd() -> anyhow::Result<()> {
let fd = match env::var("NOTIFY_FD") {
Ok(notify_fd) => notify_fd.parse()?,
+1243 -388
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -95,7 +95,7 @@ pub fn refresh(state: &mut State) {
// 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| {
state.niri.layout.with_windows(|mapped, output, _| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
+4
View File
@@ -1,3 +1,7 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
pub mod raw;
+93
View File
@@ -0,0 +1,93 @@
use mutter_x11_interop::MutterX11Interop;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use super::raw::mutter_x11_interop::v1::server::mutter_x11_interop;
const VERSION: u32 = 1;
pub struct MutterX11InteropManagerState {}
pub struct MutterX11InteropManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait MutterX11InteropHandler {}
impl MutterX11InteropManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = MutterX11InteropManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, MutterX11Interop, _>(VERSION, global_data);
Self {}
}
}
impl<D> GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData, D>
for MutterX11InteropManagerState
where
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<MutterX11Interop>,
_manager_state: &MutterX11InteropManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &MutterX11InteropManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<MutterX11Interop, (), D> for MutterX11InteropManagerState
where
D: Dispatch<MutterX11Interop, ()>,
D: MutterX11InteropHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_resource: &MutterX11Interop,
request: <MutterX11Interop as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
mutter_x11_interop::Request::Destroy => (),
mutter_x11_interop::Request::SetX11Parent { .. } => (),
}
}
}
#[macro_export]
macro_rules! delegate_mutter_x11_interop {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: $crate::protocols::mutter_x11_interop::MutterX11InteropManagerGlobalData
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: ()
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
};
}
+897
View File
@@ -0,0 +1,897 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::iter::zip;
use std::mem;
use niri_config::{FloatOrInt, OutputName, Vrr};
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 name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: name.format_make_model_serial_or_connector(),
..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 name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: name.format_make_model_serial_or_connector(),
..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 vrr = match state {
WEnum::Value(AdaptiveSyncState::Enabled) => Some(Vrr { on_demand: false }),
WEnum::Value(AdaptiveSyncState::Disabled) => None,
_ => {
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 = vrr;
}
_ => 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());
// Format matches what Output::new() does internally.
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_SERIAL_NUMBER_SINCE {
if let Some(serial) = &conf.serial {
new_head.serial_number(serial.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));
}
+25
View File
@@ -0,0 +1,25 @@
pub mod mutter_x11_interop {
pub mod v1 {
pub use self::generated::server;
mod generated {
pub mod server {
#![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
#![allow(non_upper_case_globals, non_snake_case, unused_imports)]
#![allow(missing_docs, clippy::all)]
use smithay::reexports::wayland_server;
use wayland_server::protocol::*;
pub mod __interfaces {
use smithay::reexports::wayland_server;
use wayland_server::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("resources/mutter-x11-interop.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_server_code!("resources/mutter-x11-interop.xml");
}
}
}
}
+203 -81
View File
@@ -1,7 +1,14 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
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,
@@ -11,17 +18,62 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::protocol::wl_shm::Format;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::wayland::shm;
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use smithay::wayland::{dmabuf, shm};
// We do not support copy_with_damage() semantics yet.
const VERSION: u32 = 1;
use crate::utils::get_monotonic_time;
pub struct ScreencopyManagerState;
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>,
@@ -42,7 +94,28 @@ impl ScreencopyManagerState {
};
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
Self
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()
}
}
@@ -56,14 +129,15 @@ where
D: 'static,
{
fn bind(
_state: &mut D,
state: &mut D,
_display: &DisplayHandle,
_client: &Client,
manager: New<ZwlrScreencopyManagerV1>,
_manager_state: &ScreencopyManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
let manager = data_init.init(manager, ());
state.screencopy_state().bind(&manager);
}
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
@@ -82,7 +156,7 @@ where
fn request(
_state: &mut D,
_client: &Client,
_manager: &ZwlrScreencopyManagerV1,
manager: &ZwlrScreencopyManagerV1,
request: zwlr_screencopy_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
@@ -94,7 +168,13 @@ where
overlay_cursor,
output,
} => {
let output = Output::from_resource(&output).unwrap();
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));
@@ -116,7 +196,13 @@ where
return;
}
let output = Output::from_resource(&output).unwrap();
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);
@@ -124,8 +210,8 @@ where
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let output_scale = output.current_scale().integer_scale();
let physical_rect = rect.to_physical(output_scale);
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 {
@@ -162,6 +248,7 @@ where
let frame = data_init.init(
frame,
ScreencopyFrameState::Pending {
manager: manager.clone(),
info,
copied: Arc::new(AtomicBool::new(false)),
},
@@ -169,30 +256,31 @@ where
// Send desired SHM buffer parameters.
frame.buffer(
wl_shm::Format::Argb8888,
Format::Xrgb8888,
buffer_size.w as u32,
buffer_size.h as u32,
buffer_size.w as u32 * 4,
);
// if manager.version() >= 3 {
// // Send desired DMA buffer parameters.
// frame.linux_dmabuf(
// Fourcc::Argb8888 as u32,
// buffer_size.w as u32,
// buffer_size.h as u32,
// );
//
// // Notify client that all supported buffers were enumerated.
// frame.buffer_done();
// }
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, frame: Screencopy);
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy);
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState;
}
#[allow(missing_docs)]
@@ -224,6 +312,7 @@ pub struct ScreencopyFrameInfo {
pub enum ScreencopyFrameState {
Failed,
Pending {
manager: ZwlrScreencopyManagerV1,
info: ScreencopyFrameInfo,
copied: Arc<AtomicBool>,
},
@@ -248,9 +337,13 @@ where
return;
}
let (info, copied) = match data {
ScreencopyFrameState::Failed => return,
ScreencopyFrameState::Pending { info, copied } => (info, copied),
let ScreencopyFrameState::Pending {
manager,
info,
copied,
} = data
else {
return;
};
if copied.load(Ordering::SeqCst) {
@@ -263,44 +356,71 @@ where
let (buffer, with_damage) = match request {
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
_ => unreachable!(),
};
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == info.buffer_size.w * 4
&& buffer_data.height == info.buffer_size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height
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(Screencopy {
with_damage,
buffer,
frame: frame.clone(),
info: info.clone(),
submitted: false,
});
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,
#[allow(unused)]
buffer: ScreencopyBuffer,
with_damage: bool,
buffer: WlBuffer,
submitted: bool,
}
@@ -314,7 +434,7 @@ impl Drop for Screencopy {
impl Screencopy {
/// Get the target buffer to copy to.
pub fn buffer(&self) -> &WlBuffer {
pub fn buffer(&self) -> &ScreencopyBuffer {
&self.buffer
}
@@ -334,17 +454,19 @@ impl Screencopy {
self.info.overlay_cursor
}
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
// assert!(self.with_damage);
//
// for Rectangle { loc, size } in damage {
// self.frame
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
// }
// }
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.
pub fn submit(mut self, y_invert: bool) {
fn submit(mut self, y_invert: bool, timestamp: Duration) {
// Notify client that buffer is ordinary.
self.frame.flags(if y_invert {
Flags::YInvert
@@ -353,34 +475,34 @@ impl Screencopy {
});
// Notify client about successful copy.
let time = UNIX_EPOCH.elapsed().unwrap();
let tv_sec_hi = (time.as_secs() >> 32) as u32;
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
let tv_nsec = time.subsec_nanos();
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<OwnedFd>,
// event_loop: &LoopHandle<'_, T>,
// ) {
// match sync_point {
// None => self.submit(y_invert),
// Some(sync_fd) => {
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
// let mut screencopy = Some(self);
// event_loop
// .insert_source(source, move |_, _, _| {
// screencopy.take().unwrap().submit(y_invert);
// Ok(PostAction::Remove)
// })
// .unwrap();
// }
// }
// }
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();
}
}
}
}
+746 -108
View File
File diff suppressed because it is too large Load Diff
+58 -22
View File
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
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};
@@ -26,27 +28,32 @@ pub struct BorderRenderElement {
#[derive(Debug, Clone, Copy, PartialEq)]
struct Parameters {
size: Size<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<i32, Logical>,
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<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<i32, Logical>,
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 {
@@ -54,12 +61,14 @@ impl BorderRenderElement {
params: Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
},
};
rv.update_inner();
@@ -73,12 +82,14 @@ impl BorderRenderElement {
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.,
},
}
}
@@ -90,24 +101,28 @@ impl BorderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
size: Size<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<i32, Logical>,
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;
@@ -121,12 +136,14 @@ impl BorderRenderElement {
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;
@@ -142,7 +159,7 @@ impl BorderRenderElement {
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y <= 0. {
if grad_dir.y < 0. {
grad_vec = -grad_vec;
}
@@ -154,12 +171,29 @@ impl BorderRenderElement {
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("color_from", color_from),
Uniform::new("color_to", color_to),
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()),
@@ -172,7 +206,7 @@ impl BorderRenderElement {
);
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}
@@ -239,8 +273,9 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
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)
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
@@ -255,8 +290,9 @@ impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
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)
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
+16 -10
View File
@@ -18,8 +18,10 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
inner: WaylandSurfaceRenderElement<R>,
program: GlesTexProgram,
corner_radius: CornerRadius,
geometry: Rectangle<i32, Logical>,
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)]
@@ -32,7 +34,7 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn new(
elem: WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
program: GlesTexProgram,
corner_radius: CornerRadius,
) -> Self {
@@ -76,6 +78,7 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
corner_radius,
geometry,
input_to_geo,
scale: scale.x as f32,
}
}
@@ -86,7 +89,7 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn will_clip(
elem: &WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> bool {
let elem_geo = elem.geometry(scale);
@@ -95,10 +98,10 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
if corner_radius == CornerRadius::default() {
!geo.contains_rect(elem_geo)
} else {
let corners = Self::rounded_corners(geometry.to_f64(), corner_radius);
let corners = Self::rounded_corners(geometry, corner_radius);
let corners = corners
.into_iter()
.map(|rect| rect.to_physical_precise_round(scale));
.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()
}
@@ -186,11 +189,11 @@ impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
if self.corner_radius == CornerRadius::default() {
regions.collect()
} else {
let corners = Self::rounded_corners(self.geometry.to_f64(), self.corner_radius);
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_round(scale);
let mut rect = rect.to_physical_precise_up(scale);
rect.loc -= elem_loc;
rect
});
@@ -215,10 +218,12 @@ impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
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),
@@ -227,7 +232,7 @@ impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage)?;
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.clear_tex_program_override();
Ok(())
}
@@ -248,6 +253,7 @@ impl<'render> RenderElement<TtyRenderer<'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(),
@@ -260,7 +266,7 @@ impl<'render> RenderElement<TtyRenderer<'render>>
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::draw(&self.inner, frame, src, dst, damage)?;
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.as_gles_frame().clear_tex_program_override();
Ok(())
}
@@ -276,7 +282,7 @@ impl<'render> RenderElement<TtyRenderer<'render>>
}
impl RoundedCornerDamage {
pub fn set_size(&mut self, size: Size<i32, Logical>) {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
self.damage.set_size(size);
}
+5 -4
View File
@@ -7,7 +7,7 @@ use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
pub struct ExtraDamage {
id: Id,
commit: CommitCounter,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
}
impl ExtraDamage {
@@ -19,7 +19,7 @@ impl ExtraDamage {
}
}
pub fn set_size(&mut self, size: Size<i32, Logical>) {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
if self.geometry.size == size {
return;
}
@@ -32,7 +32,7 @@ impl ExtraDamage {
self.commit.increment();
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.geometry.loc = location;
self
}
@@ -58,7 +58,7 @@ impl Element for ExtraDamage {
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_round(scale)
self.geometry.to_physical_precise_up(scale)
}
}
@@ -69,6 +69,7 @@ impl<R: Renderer> RenderElement<R> for ExtraDamage {
_src: Rectangle<f64, Buffer>,
_dst: Rectangle<i32, Physical>,
_damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), R::Error> {
Ok(())
}
+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)
}
}
+36 -41
View File
@@ -2,25 +2,27 @@ use std::ptr;
use anyhow::{ensure, Context};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
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::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::backend::renderer::{Bind, Color32F, 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;
@@ -30,7 +32,9 @@ 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)]
@@ -47,7 +51,7 @@ pub enum RenderTarget {
#[derive(Debug)]
pub struct BakedBuffer<B> {
pub buffer: B,
pub location: Point<i32, Logical>,
pub location: Point<f64, Logical>,
pub src: Option<Rectangle<f64, Logical>>,
pub dst: Option<Size<i32, Logical>>,
}
@@ -64,7 +68,7 @@ pub trait ToRenderElement {
fn to_render_element(
&self,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
@@ -116,17 +120,17 @@ impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
let elem = TextureRenderElement::from_texture_buffer(
(location + self.location).to_physical_precise_round(scale),
&self.buffer,
Some(alpha),
self.buffer.clone(),
location + self.location,
alpha,
self.src,
self.dst,
self.dst.map(|dst| dst.to_f64()),
kind,
);
PrimaryGpuTextureRenderElement(elem)
@@ -138,20 +142,12 @@ impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement {
SolidColorRenderElement::from_buffer(
&self.buffer,
(location + self.location)
.to_physical_precise_round(scale)
.to_i32_round(),
scale,
alpha,
kind,
)
SolidColorRenderElement::from_buffer(&self.buffer, location + self.location, alpha, kind)
}
}
@@ -238,16 +234,19 @@ pub fn render_to_vec(
Ok(copy.to_vec())
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
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)
}
@@ -255,32 +254,28 @@ pub fn render_to_dmabuf(
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!();
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
ensure!(
// The buffer prefers pixels in little endian ...
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == size.w * 4
buffer_data.format == wl_shm::Format::Xrgb8888
&& buffer_data.width == size.w
&& buffer_data.height == size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
&& 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)?;
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
unsafe {
let _span = tracy_client::span!("copy_nonoverlapping");
@@ -307,7 +302,7 @@ fn render_elements(
.context("error starting frame")?;
frame
.clear([0., 0., 0., 0.], &[output_rect])
.clear(Color32F::TRANSPARENT, &[output_rect])
.context("error clearing")?;
for element in elements {
@@ -317,7 +312,7 @@ fn render_elements(
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.draw(&mut frame, src, dst, &[damage], &[])
.context("error drawing element")?;
}
}
+45 -10
View File
@@ -1,6 +1,5 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
@@ -10,6 +9,7 @@ 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.
@@ -59,12 +59,17 @@ impl OffscreenRenderElement {
elements,
) {
Ok((texture, _sync_point)) => {
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let buffer = TextureBuffer::from_texture(
renderer,
texture,
scale as f64,
Transform::Normal,
Vec::new(),
);
let element = TextureRenderElement::from_texture_buffer(
geo.loc.to_f64(),
&buffer,
Some(result_alpha),
buffer,
geo.loc.to_f64().to_logical(scale as f64),
result_alpha,
None,
None,
Kind::Unspecified,
@@ -170,12 +175,27 @@ impl RenderElement<GlesRenderer> for OffscreenRenderElement {
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)?;
RenderElement::<GlesRenderer>::draw(
texture,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
}
Ok(())
}
@@ -196,12 +216,27 @@ impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
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)?;
RenderElement::<GlesRenderer>::draw(
texture,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
RenderElement::<GlesRenderer>::draw(
&self.fallback,
gles_frame,
src,
dst,
damage,
opaque_regions,
)?;
}
Ok(())
}
+6 -4
View File
@@ -1,14 +1,14 @@
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, 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)]
#[derive(Debug, Clone)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
impl Element for PrimaryGpuTextureRenderElement {
@@ -60,9 +60,10 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
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)?;
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
@@ -80,9 +81,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderEle
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)?;
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
+4 -2
View File
@@ -103,10 +103,11 @@ macro_rules! niri_render_elements {
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)
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage, opaque_regions)
})+
}
}
@@ -127,10 +128,11 @@ macro_rules! niri_render_elements {
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)
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage, opaque_regions)
})+
}
}
+2 -2
View File
@@ -17,7 +17,7 @@ pub trait NiriRenderer:
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriTextureId: Texture + Clone + Send + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
@@ -28,7 +28,7 @@ pub trait NiriRenderer:
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::TextureId: Texture + Clone + Send + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
+13 -10
View File
@@ -18,12 +18,12 @@ pub struct ResizeRenderElement(ShaderRenderElement);
impl ResizeRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
area: Rectangle<i32, Logical>,
area: Rectangle<f64, Logical>,
scale: Scale<f64>,
texture_prev: (GlesTexture, Rectangle<i32, Physical>),
size_prev: Size<i32, Logical>,
size_prev: Size<f64, Logical>,
texture_next: (GlesTexture, Rectangle<i32, Physical>),
size_next: Size<i32, Logical>,
size_next: Size<f64, Logical>,
progress: f32,
clamped_progress: f32,
corner_radius: CornerRadius,
@@ -35,17 +35,17 @@ impl ResizeRenderElement {
let (texture_prev, tex_prev_geo) = texture_prev;
let (texture_next, tex_next_geo) = texture_next;
let scale_prev = area.size.to_f64() / size_prev.to_f64();
let scale_next = area.size.to_f64() / size_next.to_f64();
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);
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).to_i32_round(),
combined_geo.size.to_logical(scale).to_i32_round(),
area.loc + combined_geo.loc.to_logical(scale),
combined_geo.size.to_logical(scale),
);
// Convert Smithay types into glam types.
@@ -87,6 +87,7 @@ impl ResizeRenderElement {
ProgramType::Resize,
area.size,
None,
scale.x,
result_alpha,
vec![
mat3_uniform("niri_input_to_curr_geo", input_to_curr_geo),
@@ -166,8 +167,9 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
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)?;
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)?;
Ok(())
}
@@ -183,9 +185,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
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)?;
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
+54 -63
View File
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::ffi::CString;
use std::rc::Rc;
use glam::{Mat3, Vec2};
@@ -23,8 +23,10 @@ pub struct ShaderRenderElement {
program: ProgramType,
id: Id,
commit_counter: CommitCounter,
area: Rectangle<i32, Logical>,
opaque_regions: Vec<Rectangle<i32, Logical>>,
area: Rectangle<f64, Logical>,
opaque_regions: Vec<Rectangle<f64, Logical>>,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
alpha: f32,
additional_uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
@@ -47,6 +49,7 @@ struct ShaderProgramInternal {
uniform_tex_matrix: ffi::types::GLint,
uniform_matrix: ffi::types::GLint,
uniform_size: ffi::types::GLint,
uniform_scale: ffi::types::GLint,
uniform_alpha: ffi::types::GLint,
attrib_vert: ffi::types::GLint,
attrib_vert_position: ffi::types::GLint,
@@ -73,35 +76,31 @@ unsafe fn compile_program(
let debug_program =
unsafe { link_program(gl, include_str!("shaders/texture.vert"), &debug_shader)? };
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
let size = CStr::from_bytes_with_nul(b"niri_size\0").expect("NULL terminated");
let alpha = CStr::from_bytes_with_nul(b"niri_alpha\0").expect("NULL terminated");
let tint = CStr::from_bytes_with_nul(b"niri_tint\0").expect("NULL terminated");
let vert = c"vert";
let vert_position = c"vert_position";
let matrix = c"matrix";
let tex_matrix = c"tex_matrix";
let size = c"niri_size";
let scale = c"niri_scale";
let alpha = c"niri_alpha";
let tint = c"niri_tint";
Ok(ShaderProgram(Rc::new(ShaderProgramInner {
normal: ShaderProgramInternal {
program,
uniform_matrix: gl
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
uniform_size: gl
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
uniform_alpha: gl
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr()),
uniform_tex_matrix: gl.GetUniformLocation(program, tex_matrix.as_ptr()),
uniform_size: gl.GetUniformLocation(program, size.as_ptr()),
uniform_scale: gl.GetUniformLocation(program, scale.as_ptr()),
uniform_alpha: gl.GetUniformLocation(program, alpha.as_ptr()),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
attrib_vert_position: gl.GetAttribLocation(program, vert_position.as_ptr()),
additional_uniforms: additional_uniforms
.iter()
.map(|uniform| {
let name =
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
let location = gl.GetUniformLocation(program, name.as_ptr());
(
uniform.name.clone().into_owned(),
UniformDesc {
@@ -115,39 +114,26 @@ unsafe fn compile_program(
.iter()
.map(|name_| {
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
let location = gl.GetUniformLocation(program, name.as_ptr());
(name_.to_string(), location)
})
.collect(),
},
debug: ShaderProgramInternal {
program: debug_program,
uniform_matrix: gl
.GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl.GetUniformLocation(
debug_program,
tex_matrix.as_ptr() as *const ffi::types::GLchar,
),
uniform_size: gl
.GetUniformLocation(debug_program, size.as_ptr() as *const ffi::types::GLchar),
uniform_alpha: gl
.GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar),
attrib_vert: gl
.GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl.GetAttribLocation(
debug_program,
vert_position.as_ptr() as *const ffi::types::GLchar,
),
uniform_matrix: gl.GetUniformLocation(debug_program, matrix.as_ptr()),
uniform_tex_matrix: gl.GetUniformLocation(debug_program, tex_matrix.as_ptr()),
uniform_size: gl.GetUniformLocation(debug_program, size.as_ptr()),
uniform_scale: gl.GetUniformLocation(debug_program, scale.as_ptr()),
uniform_alpha: gl.GetUniformLocation(debug_program, alpha.as_ptr()),
attrib_vert: gl.GetAttribLocation(debug_program, vert.as_ptr()),
attrib_vert_position: gl.GetAttribLocation(debug_program, vert_position.as_ptr()),
additional_uniforms: additional_uniforms
.iter()
.map(|uniform| {
let name =
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
let location = gl.GetUniformLocation(
debug_program,
name.as_ptr() as *const ffi::types::GLchar,
);
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
(
uniform.name.clone().into_owned(),
UniformDesc {
@@ -161,16 +147,12 @@ unsafe fn compile_program(
.iter()
.map(|name_| {
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
let location = gl.GetUniformLocation(
debug_program,
name.as_ptr() as *const ffi::types::GLchar,
);
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
(name_.to_string(), location)
})
.collect(),
},
uniform_tint: gl
.GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar),
uniform_tint: gl.GetUniformLocation(debug_program, tint.as_ptr()),
})))
}
@@ -198,10 +180,12 @@ impl ShaderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
program: ProgramType,
size: Size<i32, Logical>,
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
alpha: f32,
uniforms: Vec<Uniform<'_>>,
additional_uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
kind: Kind,
) -> Self {
@@ -209,10 +193,11 @@ impl ShaderRenderElement {
program,
id: Id::new(),
commit_counter: CommitCounter::default(),
area: Rectangle::from_loc_and_size((0, 0), size),
area: Rectangle::from_loc_and_size((0., 0.), size),
opaque_regions: opaque_regions.unwrap_or_default(),
scale,
alpha,
additional_uniforms: uniforms.into_iter().map(|u| u.into_owned()).collect(),
additional_uniforms,
textures,
kind,
}
@@ -225,6 +210,7 @@ impl ShaderRenderElement {
commit_counter: CommitCounter::default(),
area: Rectangle::default(),
opaque_regions: vec![],
scale: 1.,
alpha: 1.,
additional_uniforms: vec![],
textures: HashMap::new(),
@@ -238,20 +224,22 @@ impl ShaderRenderElement {
pub fn update(
&mut self,
size: Size<i32, Logical>,
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
uniforms: Vec<Uniform<'_>>,
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
scale: f32,
uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
) {
self.area.size = size;
self.opaque_regions = opaque_regions.unwrap_or_default();
self.additional_uniforms = uniforms.into_iter().map(|u| u.into_owned()).collect();
self.scale = scale;
self.additional_uniforms = uniforms;
self.textures = textures;
self.commit_counter.increment();
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.area.loc = location;
self
}
@@ -277,7 +265,7 @@ impl Element for ShaderRenderElement {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.opaque_regions
.iter()
.map(|region| region.to_physical_precise_round(scale))
.map(|region| region.to_physical_precise_down(scale))
.collect()
}
@@ -297,6 +285,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
src: Rectangle<f64, Buffer>,
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let frame = frame.as_gles_frame();
@@ -421,6 +410,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
tex_matrix.as_ref().as_ptr(),
);
gl.Uniform2f(program.uniform_size, dest.size.w as f32, dest.size.h as f32);
gl.Uniform1f(program.uniform_scale, self.scale);
gl.Uniform1f(program.uniform_alpha, self.alpha);
let tint = if has_tint { 1.0f32 } else { 0.0f32 };
@@ -526,10 +516,11 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ShaderRenderElement {
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage)?;
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
Ok(())
}
+177 -3
View File
@@ -1,14 +1,17 @@
precision mediump float;
precision highp float;
#if defined(DEBUG_FLAGS)
uniform float niri_tint;
#endif
uniform float niri_alpha;
uniform float niri_scale;
uniform vec2 niri_size;
varying vec2 niri_v_coords;
uniform float colorspace;
uniform float hue_interpolation;
uniform vec4 color_from;
uniform vec4 color_to;
uniform vec2 grad_offset;
@@ -20,6 +23,176 @@ uniform vec2 geo_size;
uniform vec4 outer_radius;
uniform float border_width;
vec4 premul_rect(vec4 color) {
color.rgb *= color.a;
return color;
}
vec4 premul_lch(vec4 color) {
color.xy *= color.a;
return color;
}
vec4 unpremul_rect(vec4 color) {
if (color.a == 0.0)
return color;
color.rgb /= color.a;
return color;
}
vec4 unpremul_lch(vec4 color) {
if (color.a == 0.0)
return color;
color.xy /= color.a;
return color;
}
vec4 premul_mix_unpremul_rect(vec4 color1, vec4 color2, float ratio) {
vec4 mixed = mix(premul_rect(color1), premul_rect(color2), ratio);
return unpremul_rect(mixed);
}
vec4 premul_mix_unpremul_lch(vec4 color1, vec4 color2, float ratio) {
vec4 mixed = mix(premul_lch(color1), premul_lch(color2), ratio);
return unpremul_lch(mixed);
}
vec3 srgb_to_linear(vec3 color) {
return pow(color, vec3(2.2));
}
vec3 linear_to_srgb(vec3 color) {
return pow(color, vec3(1.0 / 2.2));
}
vec3 lab_to_lch(vec3 color) {
float c = sqrt(pow(color.y, 2.0) + pow(color.z, 2.0));
float h = degrees(atan(color.z, color.y)) ;
h += h <= 0.0 ?
360.0 :
0.0 ;
return vec3(
color.x,
c,
h
);
}
vec3 lch_to_lab(vec3 color) {
float a = color.y * clamp(cos(radians(color.z)), -1.0, 1.0);
float b = color.y * clamp(sin(radians(color.z)), -1.0, 1.0);
return vec3(
color.x,
a,
b
);
}
vec3 linear_to_oklab(vec3 color){
mat3 rgb_to_lms = mat3(
vec3(0.4122214708, 0.5363325363, 0.0514459929),
vec3(0.2119034982, 0.6806995451, 0.1073969566),
vec3(0.0883024619, 0.2817188376, 0.6299787005)
);
mat3 lms_to_oklab = mat3(
vec3(0.2104542553, 0.7936177850, -0.0040720468),
vec3(1.9779984951, -2.4285922050, 0.4505937099),
vec3(0.0259040371, 0.7827717662, -0.8086757660)
);
vec3 lms = color * rgb_to_lms;
lms = pow(lms, vec3(1.0 / 3.0));
return lms * lms_to_oklab;
}
vec3 oklab_to_linear(vec3 color){
mat3 oklab_to_lms = mat3(
vec3(1.0, 0.3963377774, 0.2158037573),
vec3(1.0, -0.1055613458, -0.0638541728),
vec3(1.0, -0.0894841775, -1.2914855480)
);
mat3 lms_to_rgb = mat3(
vec3(4.0767416621, -3.3077115913, 0.2309699292),
vec3(-1.2684380046, 2.6097574011, -0.3413193965),
vec3(-0.0041960863, -0.7034186147, 1.7076147010)
);
vec3 lms = color * oklab_to_lms;
lms = pow(lms, vec3(3.0));
return lms * lms_to_rgb;
}
vec4 color_mix(vec4 color1, vec4 color2, float color_ratio) {
vec4 color_out;
// srgb
if (colorspace == 0.0) {
return mix(premul_rect(color1), premul_rect(color2), color_ratio);
}
color1.rgb = srgb_to_linear(color1.rgb);
color2.rgb = srgb_to_linear(color2.rgb);
// srgb-linear
if (colorspace == 1.0) {
color_out = premul_mix_unpremul_rect(color1, color2, color_ratio);
// oklab
} else if (colorspace == 2.0) {
color1.xyz = linear_to_oklab(color1.rgb);
color2.xyz = linear_to_oklab(color2.rgb);
color_out = premul_mix_unpremul_rect(color1, color2, color_ratio);
color_out.rgb = oklab_to_linear(color_out.xyz);
// oklch
} else if (colorspace == 3.0) {
color1.xyz = lab_to_lch(linear_to_oklab(color1.rgb));
color2.xyz = lab_to_lch(linear_to_oklab(color2.rgb));
color_out = premul_mix_unpremul_lch(color1, color2, color_ratio);
float min_hue = min(color1.z, color2.z);
float max_hue = max(color1.z, color2.z);
float path_direct_distance = (max_hue - min_hue) * color_ratio;
float path_mod_distance = (360.0 - max_hue + min_hue) * color_ratio;
float path_mod =
color1.z == min_hue ?
mod(color1.z - path_mod_distance, 360.0) :
mod(color1.z + path_mod_distance, 360.0) ;
float path_direct =
color1.z == min_hue ?
color1.z + path_direct_distance :
color1.z - path_direct_distance ;
// shorter
if (hue_interpolation == 0.0) {
color_out.z =
max_hue - min_hue > 360.0 - max_hue + min_hue ?
path_mod :
path_direct ;
// longer
} else if (hue_interpolation == 1.0) {
color_out.z =
max_hue - min_hue <= 360.0 - max_hue + min_hue ?
path_mod :
path_direct ;
// increasing
} else if (hue_interpolation == 2.0) {
color_out.z =
color1.z > color2.z ?
path_mod :
path_direct ;
// decreasing
} else if (hue_interpolation == 3.0) {
color_out.z =
color1.z <= color2.z ?
path_mod :
path_direct ;
}
color_out.rgb = clamp(oklab_to_linear(lch_to_lab(color_out.xyz)), 0.0, 1.0);
}
return premul_rect(vec4(linear_to_srgb(color_out.rgb), color_out.a));
}
vec4 gradient_color(vec2 coords) {
coords = coords + grad_offset;
@@ -32,7 +205,7 @@ vec4 gradient_color(vec2 coords) {
frac += 1.0;
frac = clamp(frac, 0.0, 1.0);
return mix(color_from, color_to, frac);
return color_mix(color_from, color_to, frac);
}
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
@@ -56,7 +229,8 @@ float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
}
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
@@ -6,7 +6,7 @@
#extension GL_OES_EGL_image_external : require
#endif
precision mediump float;
precision highp float;
#if defined(EXTERNAL)
uniform samplerExternalOES tex;
#else
@@ -20,6 +20,8 @@ varying vec2 v_coords;
uniform float tint;
#endif
uniform float niri_scale;
uniform vec2 geo_size;
uniform vec4 corner_radius;
uniform mat3 input_to_geo;
@@ -45,7 +47,8 @@ float rounding_alpha(vec2 coords, vec2 size) {
}
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
@@ -1,4 +1,4 @@
precision mediump float;
precision highp float;
#if defined(DEBUG_FLAGS)
uniform float niri_tint;
@@ -18,4 +18,5 @@ uniform float niri_clamped_progress;
uniform float niri_random_seed;
uniform float niri_alpha;
uniform float niri_scale;
+3
View File
@@ -34,6 +34,8 @@ impl Shaders {
renderer,
include_str!("border.frag"),
&[
UniformName::new("colorspace", UniformType::_1f),
UniformName::new("hue_interpolation", UniformType::_1f),
UniformName::new("color_from", UniformType::_4f),
UniformName::new("color_to", UniformType::_4f),
UniformName::new("grad_offset", UniformType::_2f),
@@ -55,6 +57,7 @@ impl Shaders {
.compile_custom_texture_shader(
include_str!("clipped_surface.frag"),
&[
UniformName::new("niri_scale", UniformType::_1f),
UniformName::new("geo_size", UniformType::_2f),
UniformName::new("corner_radius", UniformType::_4f),
UniformName::new("input_to_geo", UniformType::Matrix3x3),
+2 -1
View File
@@ -1,4 +1,4 @@
precision mediump float;
precision highp float;
#if defined(DEBUG_FLAGS)
uniform float niri_tint;
@@ -18,4 +18,5 @@ uniform float niri_clamped_progress;
uniform float niri_random_seed;
uniform float niri_alpha;
uniform float niri_scale;
@@ -1,4 +1,4 @@
precision mediump float;
precision highp float;
#if defined(DEBUG_FLAGS)
uniform float niri_tint;
@@ -25,6 +25,7 @@ uniform vec4 niri_corner_radius;
uniform float niri_clip_to_geometry;
uniform float niri_alpha;
uniform float niri_scale;
float niri_rounding_alpha(vec2 coords, vec2 size) {
vec2 center;
@@ -47,5 +48,6 @@ float niri_rounding_alpha(vec2 coords, vec2 size) {
}
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
+3 -3
View File
@@ -25,7 +25,7 @@ pub struct RenderSnapshot<C, B> {
pub block_out_from: Option<BlockOutFrom>,
/// Visual size of the element at the point of the snapshot.
pub size: Size<i32, Logical>,
pub size: Size<f64, Logical>,
/// Contents rendered into a texture (lazily).
pub texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
@@ -55,7 +55,7 @@ where
.blocked_out_contents
.iter()
.map(|baked| {
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
baked.to_render_element(Point::from((0., 0.)), scale, 1., Kind::Unspecified)
})
.collect();
@@ -81,7 +81,7 @@ where
.contents
.iter()
.map(|baked| {
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
baked.to_render_element(Point::from((0., 0.)), scale, 1., Kind::Unspecified)
})
.collect();
+170
View File
@@ -0,0 +1,170 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::{Color32F, Frame as _, Renderer};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
/// Smithay's solid color buffer, but with fractional scale.
#[derive(Debug, Clone)]
pub struct SolidColorBuffer {
id: Id,
size: Size<f64, Logical>,
commit: CommitCounter,
color: Color32F,
}
/// Render element for a [`SolidColorBuffer`].
#[derive(Debug, Clone)]
pub struct SolidColorRenderElement {
id: Id,
geometry: Rectangle<f64, Logical>,
commit: CommitCounter,
color: Color32F,
kind: Kind,
}
impl Default for SolidColorBuffer {
fn default() -> Self {
Self {
id: Id::new(),
size: Default::default(),
commit: Default::default(),
color: Default::default(),
}
}
}
impl SolidColorBuffer {
pub fn new(size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) -> Self {
SolidColorBuffer {
id: Id::new(),
color: color.into(),
commit: CommitCounter::default(),
size: size.into(),
}
}
pub fn resize(&mut self, size: impl Into<Size<f64, Logical>>) {
let size = size.into();
if size != self.size {
self.size = size;
self.commit.increment();
}
}
pub fn set_color(&mut self, color: impl Into<Color32F>) {
let color = color.into();
if color != self.color {
self.color = color;
self.commit.increment();
}
}
pub fn update(&mut self, size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) {
let size = size.into();
let color = color.into();
if size != self.size || color != self.color {
self.size = size;
self.color = color;
self.commit.increment();
}
}
pub fn color(&self) -> Color32F {
self.color
}
pub fn size(&self) -> Size<f64, Logical> {
self.size
}
}
impl SolidColorRenderElement {
pub fn from_buffer(
buffer: &SolidColorBuffer,
location: impl Into<Point<f64, Logical>>,
alpha: f32,
kind: Kind,
) -> Self {
let geo = Rectangle::from_loc_and_size(location, buffer.size());
let color = buffer.color * alpha;
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
}
pub fn new(
id: Id,
geometry: Rectangle<f64, Logical>,
commit: CommitCounter,
color: Color32F,
kind: Kind,
) -> Self {
SolidColorRenderElement {
id,
geometry,
commit,
color,
kind,
}
}
pub fn color(&self) -> Color32F {
self.color
}
pub fn geo(&self) -> Rectangle<f64, Logical> {
self.geometry
}
}
impl Element for SolidColorRenderElement {
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_round(scale)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if self.color.is_opaque() {
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
.to_physical_precise_down(scale);
OpaqueRegions::from_slice(&[rect])
} else {
OpaqueRegions::default()
}
}
fn alpha(&self) -> f32 {
self.color.a()
}
fn kind(&self) -> Kind {
self.kind
}
}
impl<R: Renderer> RenderElement<R> for SolidColorRenderElement {
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 as Renderer>::Error> {
frame.draw_solid(dst, damage, self.color)
}
#[inline]
fn underlying_storage(&self, _renderer: &mut R) -> Option<UnderlyingStorage<'_>> {
None
}
}

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