Compare commits

..

494 Commits

Author SHA1 Message Date
Ivan Molodetskikh 75c79116a7 Bump version to 0.1.10-1
Uhh, apparently cargo doesn't like four-component versions.
2024-11-13 10:39:54 +03:00
Ivan Molodetskikh 4f44ef081f Guard against closed screenshot UI in its binds
They can trigger with closed screenshot UI via key repeat.
2024-11-13 10:24:21 +03:00
Ramses 4fc76b50d0 Unhide the pointer on scroll events (#797)
* Unhide the pointer on scroll events

Since we reset the surface under the pointer when we hide the pointer
(see update_pointer_contents), scroll events don't work when the pointer
is hidden.
So to make scrolling work, we make sure that we unhide the pointer when
a scrolling event occurs.

* Update src/input/mod.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-13 10:24:06 +03:00
Ivan Molodetskikh e1f065ac23 Start interactive move on Mod+Touch 2024-11-13 10:23:51 +03:00
Ivan Molodetskikh 7cc10ce1b5 Fix scrolling not working with missing mouse config 2024-11-13 10:23:44 +03:00
Ivan Molodetskikh 9d8f640503 niri-ipc: Document features 2024-11-09 17:57:52 +03:00
Ivan Molodetskikh b18cfbae23 niri-ipc: Add README and Cargo.toml metadata 2024-11-09 17:57:34 +03:00
Ivan Molodetskikh f64e7e14c3 Bump version to 0.1.10 2024-11-09 17:35:31 +03:00
Ivan Molodetskikh e8c9bfc06a wiki: Add scroll-button to mouse and touchpad overview 2024-11-09 17:23:59 +03:00
Ivan Molodetskikh 07452f50a8 Update dependencies 2024-11-09 15:57:17 +03:00
Ivan Molodetskikh 642c5acebb wiki: Remove outdated info from Application Issues 2024-11-09 11:04:39 +03:00
Ivan Molodetskikh 0886dedff1 wiki: Mention Xwayland on other pages 2024-11-09 11:04:39 +03:00
Ivan Molodetskikh cc88a7d42e default-config: Bind Ctrl-Alt-Del to quit
This seems to be a shared bind across compositors.
2024-11-09 10:29:13 +03:00
Ivan Molodetskikh c0829087da Lock session right away with no outputs 2024-11-08 16:25:06 +03:00
Ivan Molodetskikh b6f6d6a7c2 wiki: Update getting started 2024-11-08 09:43:43 +03:00
Ivan Molodetskikh 5ff8b89aaf Rework output connection to always go through on_output_config_changed()
This has the following benefits:
1. connector_connected() is now more closely mirroring
   connector_disconnected() in that it merely lights up the connector,
   and doesn't check if the connector should be off from the config.
2. We can use more complex on/off logic that depends on multiple
   connectors. For example, this commit adds logic to only disable the
   laptop panel on lid close if there are other connected outputs.

We don't want to disable the laptop panel on lid close if it's the only
connected output because it causes screen lockers to create their
surface from scratch on normal laptop unsuspend, which is undesirable
and also confuses some screen lockers.
2024-11-08 09:11:56 +03:00
Ivan Molodetskikh 927abad4b4 Only call on_output_config_changed() on lid switch
We don't need to reload the niri output config.
2024-11-08 09:11:28 +03:00
Ivan Molodetskikh 3d31f9860a Extract format_make_model_serial() 2024-11-08 09:10:54 +03:00
Ivan Molodetskikh 8867a4f84c Add disable-monitor-names debug flag 2024-11-06 08:42:22 +03:00
Ivan Molodetskikh 88f4c1d610 layout: Preserve active workspace for removed outputs 2024-11-05 21:52:02 +03:00
Ivan Molodetskikh ddcb5c5e10 layout: Move some types further down 2024-11-05 21:08:50 +03:00
Ivan Molodetskikh cd90dfc7be Disable laptop panel when the lid is closed 2024-11-05 10:03:51 +03:00
Ivan Molodetskikh a778ab3897 Extract is_laptop_panel() to utils 2024-11-05 09:40:12 +03:00
Ivan Molodetskikh 4c2f49d566 wiki: Add Since to switch events 2024-11-03 23:00:18 +03:00
Ivan Molodetskikh 49d7052bb3 wiki: Add trackball section to config overview 2024-11-03 22:58:18 +03:00
Ivan Molodetskikh 07be7e7eae wiki: Add Since to scroll-button 2024-11-03 22:56:49 +03:00
Ivan Molodetskikh 97c8717d1e wiki: Mention insert-hint config on the gestures page 2024-11-03 22:52:49 +03:00
Ivan Molodetskikh 3ac0a751fe wiki: Add Since to scroll-factor 2024-11-03 22:50:15 +03:00
elipp 8b39f986d9 Implement scroll_factor mouse and touchpad setting (#730)
* Implement scroll_factor mouse and touchpad setting

* Change to FloatOrInt, add docs

* Also change v120 values

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-11-03 18:43:03 +00:00
Christian Meissl 354c365a03 xdg: cleanup activation tokens
valid tokens will stay around until explicitly cleaned-up.
remove the token after it has been successfully used
or we consider it timed out to prevent leaking the memory
used by the activation tokens
2024-11-03 09:13:41 -08:00
Ivan Molodetskikh e0ebf1bdff Remove pointer_grab_ongoing in favor of checking the actual grab 2024-11-03 10:23:21 +03:00
Ivan Molodetskikh 11633aef98 Use is() instead of downcast().is_some() 2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 9193245871 Correct pointer constraint activation logic
Internally it uses the pointer focus, so make sure we have up-to-date
focus before setting it.
2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 7baf10b751 Clarify redraw in refresh_pointer_focus() 2024-11-03 10:15:19 +03:00
Ivan Molodetskikh f5d91c5ecc Rename pointer_focus to pointer_contents, clarify comments
This is not pointer focus and it shouldn't be pointer focus, let's be
clear about it.
2024-11-03 10:15:19 +03:00
Ivan Molodetskikh 69e3edb5a3 Rename surface_under_and_global_space() to contents_under() 2024-11-03 08:50:17 +03:00
LoipesMas d58bb4eaa3 flake: set RUSTFLAGS instead of CARGO_BUILD_RUSTFLAGS 2024-11-02 12:35:04 -07:00
LoipesMas c5fe25f422 flake: libseat has been renamed to seatd 2024-11-02 12:35:04 -07:00
Ivan Molodetskikh 600cffb009 Update Smithay (lock leak fix) 2024-11-02 18:55:56 +03:00
Christian Meissl b9d14a9eda portal: prefer gtk for access portal
using gnome for the access portal does not work,
so just override by directly using the gtk one
2024-11-02 07:55:37 -07:00
Ivan Molodetskikh 0e7e398df3 Replace current_state() with with_toplevel_role()
Avoid microallocations that happen in current_state().
2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 86bdc6898b Add with_toplevel_role() util function 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh e5ca335115 Add Tracy allocation profiling feature flag 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh fce5d66878 Follow window corner radius in insert hint 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 05d218113c Add gradient support for the insert hint
Implement it via FocusRing which already handles SolidColor vs. Border
render element.
2024-11-02 10:53:55 +03:00
Ivan Molodetskikh ef6af6adc1 Change TODO to FIXME 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh 6632699e00 Remove obsolete TODO 2024-11-02 10:53:55 +03:00
Ivan Molodetskikh d3e72245b0 Don't show the cursor on programmatic movement
For keyboard-only use, especially with warp-mouse-to-focus, the
intention is that the cursor stays hidden from keyboard and other
automatic actions, and only shows up with an actual mouse movement.
2024-10-29 21:52:03 -07:00
Ivan Molodetskikh 13fe9c8ac3 [cfg-breaking] Rename hide-on-key-press to hide-when-typing
I originally preferred on-key-press, but when-typing feels more natural
and matches sway. This setting had not been in a stable release yet so
this is not stable release cfg breaking.
2024-10-29 21:52:03 -07: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
Ivan Molodetskikh d96a66ddff Update README 2024-05-18 15:00:39 +04:00
Ivan Molodetskikh bfaf9ae060 Bump version to 0.1.6 2024-05-18 14:35:42 +04:00
Ivan Molodetskikh 2da0aaace8 wiki: Update different-corner-radius image 2024-05-18 10:50:11 +04:00
Ivan Molodetskikh ee12bbc9ed wiki: Change two instances of Telegram to Fractal 2024-05-18 09:44:48 +04:00
Ivan Molodetskikh cc4026f588 wiki: Add since to interactive resize 2024-05-18 08:59:11 +04:00
Ivan Molodetskikh aa74120143 wiki: Fix typo 2024-05-18 08:50:17 +04:00
Ivan Molodetskikh 473ef22de2 Redraw on lock surface children commits 2024-05-17 15:59:49 +04:00
Ivan Molodetskikh d76b213e03 Update Smithay (session-lock fix) 2024-05-17 15:49:02 +04:00
Ivan Molodetskikh 4dc7a6ceb8 Rearrange CLI subcommands a bit 2024-05-17 10:33:00 +03:00
rustysec 36d3e70f11 Implement niri msg workspaces 2024-05-17 10:33:00 +03:00
Ivan Molodetskikh a2f74c9bff Update Smithay (buffer leak and crash fix) 2024-05-17 07:54:56 +04:00
Ivan Molodetskikh 0ce08b598c Bump package versions 2024-05-16 18:04:18 +04:00
Ivan Molodetskikh ae63773737 Update Smithay and other deps 2024-05-16 18:00:28 +04:00
Ivan Molodetskikh c5ca412829 wiki: Add since to named workspaces 2024-05-16 13:04:51 +04:00
Ivan Molodetskikh cbfc682f9a Implement at-startup window rule 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh c64d9e5223 Fix missing check in Match PartialEq 2024-05-16 12:27:09 +04:00
Ivan Molodetskikh 4e31f7e047 wiki: Document named workspaces 2024-05-16 01:24:34 -07:00
Ivan Molodetskikh 109d99fe82 Make workspace names case-insensitive 2024-05-16 01:24:34 -07:00
Gergely Nagy eb9bbe3352 Implement named workspaces
This is an implementation of named, pre-declared workspaces. With this
implementation, workspaces can be declared in the configuration file by
name:

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

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

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

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

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

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

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

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

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

Fixes https://github.com/YaLTeR/niri/issues/221
2024-04-23 00:09:42 -07:00
Kirill Chibisov c2d03d82ce Use PopupKind instead of PopupSurface 2024-04-23 00:09:42 -07:00
Ivan Molodetskikh 5299590290 Improve cropping logic in resize shader example
The previous logic failed to the left of the geometry.
2024-04-22 22:37:47 +04:00
Ivan Molodetskikh 1681ed16d9 Change custom-shader to a prelude-epilogue system 2024-04-22 19:05:11 +04:00
Ivan Molodetskikh d4bed70884 Advertise Abgr8888 and Xbgr8888 in shm 2024-04-22 17:47:12 +04:00
Ivan Molodetskikh 49f5402669 Implement window-resize custom-shader 2024-04-21 20:16:54 +04:00
Ivan Molodetskikh 2ecbb3f6f8 Remove obsolete comment 2024-04-21 12:28:49 +04:00
168 changed files with 27144 additions and 6367 deletions
+3
View File
@@ -14,6 +14,9 @@ assignees: ''
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
* Distro:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
+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
+1551 -769
View File
File diff suppressed because it is too large Load Diff
+60 -33
View File
@@ -2,22 +2,24 @@
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.5"
version = "0.1.10-1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.81"
bitflags = "2.5.0"
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
anyhow = "1.0.93"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.214", 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,45 +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"
async-channel = { version = "2.2.0", optional = true }
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.15.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.7.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.0"
fastrand = "2.2.0"
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.2"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.153"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.5", path = "niri-config" }
niri-ipc = { version = "0.1.5", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.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"
libc = "0.2.162"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.10-1", path = "niri-config" }
niri-ipc = { version = "0.1.10-1", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.5.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.3", 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]
@@ -96,20 +106,26 @@ features = [
]
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
xshell = "0.2.5"
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]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
dbus = ["zbus", "async-io", "notify-rust", "url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables the on-demand Tracy profiler instrumentation.
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
# Enables Tracy allocation profiling.
profile-with-tracy-allocations = ["profile-with-tracy"]
# Enables dinit integration (global environment).
dinit = []
@@ -123,7 +139,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.5"
version = "0.1.10.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -135,3 +151,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"],
]
+21 -8
View File
@@ -7,10 +7,10 @@
</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/2b246c2c-7cf3-4a11-96eb-ad0c7f2f4ed6)
![](https://github.com/YaLTeR/niri/assets/1794388/52c799a1-77ec-455f-b4aa-f3236a144964)
## About
@@ -31,11 +31,12 @@ 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 gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
- [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
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e)
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- Live-reloading config
## Video Demo
@@ -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 -79
View File
@@ -1,106 +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,
seatd,
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
seatd
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
];
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()`
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"`
CARGO_BUILD_RUSTFLAGS = niri.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.5", path = "../niri-ipc" }
regex = "1.10.4"
niri-ipc = { version = "0.1.10-1", path = "../niri-ipc" }
regex = "1.11.1"
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"
+1801 -276
View File
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
use std::fs;
use std::path::PathBuf;
struct KdlCodeBlock {
filename: String,
code: String,
line_number: usize,
must_fail: bool,
}
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
let mut lines = file_contents
.lines()
.map(|line| {
// Removes the > from callouts that might contain ```kdl```
let line = line.trim();
if line.starts_with('>') {
if line.len() == 1 {
""
} else {
&line[2..]
}
} else {
line
}
})
.enumerate();
let mut kdl_code_blocks = vec![];
while let Some((line_number, line)) = lines.next() {
if !line.starts_with("```kdl") {
continue;
}
let mut snippet = String::new();
for (_, line) in lines
.by_ref()
.take_while(|(_, line)| !line.starts_with("```"))
{
snippet.push_str(line);
snippet.push('\n');
}
kdl_code_blocks.push(KdlCodeBlock {
code: snippet,
line_number,
filename: filename.to_string(),
must_fail: line.contains("must-fail"),
});
}
kdl_code_blocks
}
#[test]
fn wiki_docs_parses() {
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
let code_blocks = fs::read_dir(wiki_dir)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
.filter(|file| {
file.path()
.extension()
.map(|ext| ext == "md")
.unwrap_or(false)
})
.flat_map(|file| {
let file_contents = fs::read_to_string(file.path()).unwrap();
let file_path = file.path();
let filename = file_path.to_str().unwrap();
extract_kdl_from_file(&file_contents, filename)
});
let mut errors = vec![];
for KdlCodeBlock {
code,
line_number,
filename,
must_fail,
} in code_blocks
{
if let Err(error) = niri_config::Config::parse(&filename, &code) {
if !must_fail {
errors.push(format!(
"Error parsing wiki KDL code block at {}:{}: {:?}",
filename,
line_number,
miette::Report::new(error)
));
}
} else if must_fail {
errors.push(format!(
"Expected error parsing wiki KDL code block at {}:{}",
filename, line_number
));
}
}
if !errors.is_empty() {
panic!(
"Errors parsing {} wiki KDL code blocks:\n{}",
errors.len(),
errors.join("\n")
);
}
}
+7 -1
View File
@@ -1,16 +1,22 @@
[package]
name = "niri-ipc"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
description = "Types and helpers for interfacing with the niri Wayland compositor."
keywords = ["wayland"]
categories = ["api-bindings", "os"]
readme = "README.md"
[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"]
+16
View File
@@ -0,0 +1,16 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
## 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.
Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=0.1.10"
```
+650 -76
View File
@@ -1,4 +1,38 @@
//! 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.
//!
//! Use an exact version requirement to avoid breaking changes:
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=0.1.10"
//! ```
//!
//! ## Features
//!
//! This crate defines the following features:
//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
//! the types.
//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
#![warn(missing_docs)]
use std::collections::HashMap;
@@ -6,20 +40,55 @@ 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.
Action(Action),
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
/// configuration subsequently changes in the config file, these temporary changes will be
/// forgotten.
Output {
/// Output name.
output: String,
/// Configuration to apply.
action: OutputAction,
},
/// 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,
}
@@ -36,6 +105,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,
@@ -43,10 +113,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),
}
/// Actions that niri can perform.
@@ -56,6 +136,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 {
@@ -64,135 +145,257 @@ 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.
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: Vec<String>,
},
/// Do a screen transition.
DoScreenTransition {
/// Delay in milliseconds for the screen to freeze before starting the transition.
#[cfg_attr(feature = "clap", arg(short, long))]
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot,
Screenshot {},
/// 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,
/// Focus a workspace by index.
FocusWorkspaceUp {},
/// Focus a workspace by reference (index or name).
FocusWorkspace {
/// Index of the workspace to focus.
/// Reference (index or name) of the workspace to focus.
#[cfg_attr(feature = "clap", arg())]
index: u8,
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 index.
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 {
/// Index of the target workspace.
/// 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())]
index: u8,
reference: WorkspaceReferenceArg,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceDown {},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
/// Move the focused column to a workspace by index.
MoveColumnToWorkspaceUp {},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Index of the target workspace.
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
index: u8,
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 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.
@@ -206,21 +409,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 {},
/// Toggle visualization of output damage.
DebugToggleDamage {},
}
/// Change in window or column size.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum SizeChange {
/// Set the size in logical pixels.
SetFixed(i32),
@@ -232,8 +440,21 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// 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.
Name(String),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum LayoutSwitchTarget {
/// The next configured layout.
Next,
@@ -241,8 +462,135 @@ pub enum LayoutSwitchTarget {
Prev,
}
/// Output actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most thigs from
// niri-config should be present here.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum OutputAction {
/// Turn off the output.
Off,
/// Turn on the output.
On,
/// Set the output mode.
Mode {
/// Mode to set, or "auto" for automatic selection.
///
/// Run `niri msg outputs` to see the available modes.
#[cfg_attr(feature = "clap", arg())]
mode: ModeToSet,
},
/// Set the output scale.
Scale {
/// Scale factor to set, or "auto" for automatic selection.
#[cfg_attr(feature = "clap", arg())]
scale: ScaleToSet,
},
/// Set the output transform.
Transform {
/// Transform to set, counter-clockwise.
#[cfg_attr(feature = "clap", arg())]
transform: Transform,
},
/// Set the output position.
Position {
/// Position to set, or "auto" for automatic selection.
#[cfg_attr(feature = "clap", command(subcommand))]
position: PositionToSet,
},
/// Set the variable refresh rate mode.
Vrr {
/// 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,
/// Specific mode.
Specific(ConfiguredMode),
}
/// Output mode as set in the config file.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ConfiguredMode {
/// Width in physical pixels.
pub width: u16,
/// Height in physical pixels.
pub height: u16,
/// Refresh rate.
pub refresh: Option<f64>,
}
/// Output scale to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum ScaleToSet {
/// Niri will pick the scale automatically.
Automatic,
/// Specific scale.
Specific(f64),
}
/// Output position to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum PositionToSet {
/// Position the output automatically.
#[cfg_attr(feature = "clap", command(name = "auto"))]
Automatic,
/// Set a specific position.
#[cfg_attr(feature = "clap", command(name = "set"))]
Specific(ConfiguredPosition),
}
/// Output position as set in the config file.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ConfiguredPosition {
/// Logical X position.
pub x: i32,
/// Logical Y position.
pub y: i32,
}
/// 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,
@@ -250,6 +598,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.
@@ -269,7 +619,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,
@@ -282,7 +633,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,
@@ -300,6 +652,8 @@ 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,
@@ -315,20 +669,186 @@ pub enum Transform {
/// Flipped horizontally.
Flipped,
/// Rotated by 90° and flipped horizontally.
#[cfg_attr(feature = "clap", value(name("flipped-90")))]
Flipped90,
/// Flipped vertically.
#[cfg_attr(feature = "clap", value(name("flipped-180")))]
Flipped180,
/// Rotated by 270° and flipped horizontally.
#[cfg_attr(feature = "clap", value(name("flipped-270")))]
Flipped270,
}
/// Toplevel window.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Window {
/// 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,
/// The target output was not found, the change will be applied when it is connected.
OutputWasMissing,
}
/// A workspace.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Workspace {
/// 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>,
/// Name of the output that the workspace is on.
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace is currently active on its output.
///
/// 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 {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let reference = if let Ok(index) = s.parse::<i32>() {
if let Ok(idx) = u8::try_from(index) {
Self::Index(idx)
} else {
return Err("workspace index must be between 0 and 255");
}
} else {
Self::Name(s.to_string())
};
Ok(reference)
}
}
impl FromStr for SizeChange {
@@ -403,3 +923,57 @@ impl FromStr for Transform {
}
}
}
impl FromStr for ModeToSet {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("auto") {
return Ok(Self::Automatic);
}
let mode = s.parse()?;
Ok(Self::Specific(mode))
}
}
impl FromStr for ConfiguredMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once('x') else {
return Err("no 'x' separator found");
};
let (height, refresh) = match rest.split_once('@') {
Some((height, refresh)) => (height, Some(refresh)),
None => (rest, None),
};
let width = width.parse().map_err(|_| "error parsing width")?;
let height = height.parse().map_err(|_| "error parsing height")?;
let refresh = refresh
.map(str::parse)
.transpose()
.map_err(|_| "error parsing refresh rate")?;
Ok(Self {
width,
height,
refresh,
})
}
}
impl FromStr for ScaleToSet {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("auto") {
return Ok(Self::Automatic);
}
let scale = s.parse().map_err(|_| "error parsing scale")?;
Ok(Self::Specific(scale))
}
}
+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.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.5", path = ".." }
niri-config = { version = "0.1.5", path = "../niri-config" }
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.10-1", path = ".." }
niri-config = { version = "0.1.10-1", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+16 -11
View File
@@ -3,10 +3,11 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::gradient::GradientRenderElement;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
use smithay::utils::{Logical, Physical, Rectangle, Size};
use super::TestCase;
@@ -53,22 +54,26 @@ impl TestCase for GradientAngle {
fn render(
&mut self,
renderer: &mut GlesRenderer,
_renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0., 0.), area.size),
GradientInterpolation::default(),
Color::new_unpremul(1., 0., 0., 1.),
Color::new_unpremul(0., 1., 0., 1.),
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
+30 -23
View File
@@ -4,11 +4,11 @@ use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::gradient::GradientRenderElement;
use niri_config::Color;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use super::TestCase;
@@ -20,15 +20,14 @@ pub struct GradientArea {
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let mut border = FocusRing::new(niri_config::FocusRing {
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,
});
border.set_active(true);
Self {
progress: 0.,
@@ -76,37 +75,45 @@ 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_area = Rectangle::from_loc_and_size(g_loc, g_size);
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
g_area.loc -= area.loc;
self.border.update(g_size, true);
self.border.update_render_elements(
g_size,
true,
true,
Rectangle::default(),
CornerRadius::default(),
1.,
);
rv.extend(
self.border
.render(
renderer,
Point::from(g_loc),
Scale::from(1.),
size.to_logical(1),
)
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
[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).to_f64(),
0.,
CornerRadius::default(),
1.,
)
.with_location(area.loc)]
.into_iter()
.map(|elem| Box::new(elem) as _),
);
@@ -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()
}
}
+15 -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), 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), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout
@@ -186,17 +192,13 @@ impl TestCase for Layout {
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win.id());
self.layout.update_window(win.id(), None);
}
}
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
@@ -222,11 +224,11 @@ impl TestCase for Layout {
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
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;
+17 -13
View File
@@ -3,10 +3,10 @@ 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, Scale, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use super::TestCase;
use crate::test_window::TestWindow;
@@ -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();
}
@@ -94,7 +94,7 @@ impl TestCase for Tile {
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time, true);
self.tile.advance_animations(current_time);
}
fn render(
@@ -102,15 +102,19 @@ 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.)),
);
self.tile
.render(
renderer,
location,
Scale::from(1.),
size.to_logical(1),
true,
RenderTarget::Output,
)
+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);
+64 -38
View File
@@ -2,15 +2,19 @@ use std::cell::RefCell;
use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::RenderTarget;
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, Size, Transform};
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
@@ -35,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,
@@ -47,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]),
})),
}
}
@@ -83,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);
@@ -110,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
}
@@ -145,35 +149,41 @@ 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,
) -> Vec<LayoutElementRenderElement<R>> {
) -> SplitElements<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
]
SplitElements {
normal: vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
location
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
.into(),
],
popups: vec![],
}
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
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;
}
@@ -194,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
@@ -208,8 +218,14 @@ impl LayoutElement for TestWindow {
fn set_activated(&mut self, _active: bool) {}
fn set_active_in_column(&mut self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
@@ -220,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 {
@@ -227,10 +247,6 @@ impl LayoutElement for TestWindow {
&EMPTY
}
fn take_unmap_snapshot(&self) -> Option<LayoutElementRenderSnapshot> {
None
}
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
None
}
@@ -238,4 +254,14 @@ impl LayoutElement for TestWindow {
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
None
}
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
fn cancel_interactive_resize(&mut self) {}
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
}
+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 }}}
+43 -7
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,14 +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 { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
@@ -451,6 +486,7 @@ binds {
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
+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>
+1
View File
@@ -1,3 +1,4 @@
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk;
org.freedesktop.impl.portal.Secret=gnome-keyring;
+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>
+7 -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,
@@ -41,6 +41,7 @@ enum Kind {
#[derive(Debug, Clone, Copy)]
pub enum Curve {
Linear,
EaseOutQuad,
EaseOutCubic,
EaseOutExpo,
@@ -100,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.
@@ -291,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 } => {
@@ -358,6 +359,7 @@ impl Animation {
impl Curve {
pub fn y(self, x: f64) -> f64 {
match self {
Curve::Linear => x,
Curve::EaseOutQuad => EaseOutQuad.y(x),
Curve::EaseOutCubic => EaseOutCubic.y(x),
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
@@ -368,6 +370,7 @@ impl Curve {
impl From<niri_config::AnimationCurve> for Curve {
fn from(value: niri_config::AnimationCurve) -> Self {
match value {
niri_config::AnimationCurve::Linear => Curve::Linear,
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
+39 -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),
@@ -144,6 +167,21 @@ impl Backend {
}
}
pub fn on_debug_config_changed(&mut self) {
match self {
Backend::Tty(tty) => tty.on_debug_config_changed(),
Backend::Winit(_) => (),
}
}
pub fn tty_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
+552 -261
View File
File diff suppressed because it is too large Load Diff
+37 -8
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,10 +15,11 @@ 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};
use crate::utils::{get_monotonic_time, logical_output};
@@ -35,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(),
@@ -58,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,
@@ -97,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;
@@ -135,6 +144,20 @@ impl Winit {
resources::init(renderer);
shaders::init(renderer);
let config = self.config.borrow();
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
shaders::set_custom_resize_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
shaders::set_custom_close_program(renderer, Some(src));
}
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
shaders::set_custom_open_program(renderer, Some(src));
}
drop(config);
niri.layout.update_shaders();
niri.add_output(self.output.clone(), None, false);
}
@@ -153,13 +176,19 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
let output_state = niri.output_state.get_mut(output).unwrap();
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
}
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
+40 -9
View File
@@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -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.
@@ -32,12 +35,6 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
@@ -46,16 +43,31 @@ pub enum Sub {
#[arg(short, long)]
json: bool,
},
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
///
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
/// command line argument takes precedence.
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
pub enum Msg {
/// Print the version of the running niri instance.
Version,
/// List connected outputs.
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.
@@ -63,6 +75,25 @@ pub enum Msg {
#[command(subcommand)]
action: Action,
},
/// Change output configuration temporarily.
///
/// The configuration is changed temporarily and not saved into the config file. If the output
/// configuration subsequently changes in the config file, these temporary changes will be
/// forgotten.
Output {
/// Output name.
///
/// Run `niri msg outputs` to see the output names.
#[arg()]
output: String,
/// Configuration to apply.
#[command(subcommand)]
action: OutputAction,
},
/// 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.
RequestError,
}
+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 => (),
}
})
+67 -24
View File
@@ -8,6 +8,7 @@ use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
use crate::utils::is_laptop_panel;
pub struct DisplayConfig {
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
@@ -57,24 +58,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 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 c = &output.name;
let is_laptop_panel = is_laptop_panel(c);
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 +107,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 +148,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 +180,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.
+177 -50
View File
@@ -1,23 +1,26 @@
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,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell::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 {
@@ -36,50 +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).ok(),
_ => None,
})
});
if let Some(dmabuf) = maybe_dmabuf {
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
let client = surface.client().unwrap();
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
let display_handle = state.niri.display_handle.clone();
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);
@@ -116,22 +90,27 @@ impl CompositorHandler for State {
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, is_full_width, output) =
let (rules, width, is_full_width, output, workspace_name) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name,
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(rules, width, is_full_width, output)
// Check that the workspace still exists.
let workspace_name = workspace_name
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
(rules, width, is_full_width, output, workspace_name)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None)
(ResolvedWindowRules::empty(), None, false, None, None)
};
let parent = toplevel
@@ -148,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();
@@ -157,6 +138,13 @@ impl CompositorHandler for State {
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(workspace_name) = &workspace_name {
self.niri.layout.add_window_to_named_workspace(
workspace_name,
mapped,
width,
is_full_width,
)
} else if let Some(output) = &output {
self.niri
.layout
@@ -194,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())
@@ -203,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);
});
}
@@ -220,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();
@@ -235,8 +241,21 @@ impl CompositorHandler for State {
return;
}
let serial = with_states(surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
role.configure_serial
});
if serial.is_none() {
error!("commit on a mapped surface without a configured serial");
}
// The toplevel remains mapped.
self.niri.layout.update_window(&window);
self.niri.layout.update_window(&window, serial);
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
@@ -254,7 +273,7 @@ impl CompositorHandler for State {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.layout.update_window(&window, None);
self.niri.queue_redraw(&output);
return;
}
@@ -265,31 +284,77 @@ 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.
if self.niri.is_locked() {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == surface {
if lock_surface.wl_surface() == &root_surface {
self.niri.queue_redraw(&output.clone());
break;
return;
}
}
}
@@ -300,11 +365,17 @@ impl CompositorHandler for State {
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
// when that happens, so that the closing animation includes all these subsurfaces.
//
// Test client: alacritty with CSD.
// Test client: alacritty with CSD <= 0.13 (it was fixed in winit afterwards:
// https://github.com/rust-windowing/winit/pull/3625).
//
// This is still not perfect, as this function is called already after the (first)
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
mapped.store_unmap_snapshot_if_empty(renderer);
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
}
}
@@ -312,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);
}
}
@@ -327,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");
}
}
}
+102 -38
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, WindowSurfaceType};
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::compositor::{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);
@@ -55,58 +70,107 @@ impl WlrLayerShellHandler for State {
}
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
self.unconstrain_popup(&popup);
self.unconstrain_popup(&PopupKind::Xdg(popup));
}
}
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
}
}
+218 -39
View File
@@ -7,30 +7,36 @@ use std::io::Write;
use std::os::fd::OwnedFd;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
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,
};
@@ -47,23 +53,35 @@ 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, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
@@ -122,35 +140,100 @@ impl TabletSeatHandler for State {
delegate_tablet_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
pointer.current_location(),
&self.niri.pointer_focus,
);
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
// Pointer constraints track pointer focus internally, so make sure it's up to date before
// activating a new one.
self.refresh_pointer_contents();
self.niri.maybe_activate_pointer_constraint();
}
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;
}
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
// will change, even though the real pointer focus remains on the Blender surface due to
// the click grab.
//
// Ideally we would just use the constraint surface, but we need its origin. So this is
// more of a hack because pointer contents has the surface origin available.
//
// FIXME: use the constraint surface somehow, don't use pointer contents.
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
return;
};
if surface_under_pointer != 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);
impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
let popup = PopupKind::from(surface.clone());
let popup = PopupKind::InputMethod(surface);
if let Some(output) = self.output_for_popup(&popup) {
let scale = output.current_scale().integer_scale();
let scale = output.current_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
let wl_surface = popup.wl_surface();
with_states(wl_surface, |data| {
send_surface_state(wl_surface, data, scale, transform);
send_scale_transform(wl_surface, data, scale, transform);
});
}
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking ime popup {err:?}");
}
}
fn popup_repositioned(&mut self, surface: PopupSurface) {
let popup = PopupKind::InputMethod(surface);
self.unconstrain_popup(&popup);
}
fn dismiss_popup(&mut self, surface: PopupSurface) {
if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) {
let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
}
}
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
self.niri
.layout
@@ -178,6 +261,10 @@ impl SelectionHandler for State {
let buf = user_data.clone();
thread::spawn(move || {
// Clear O_NONBLOCK, otherwise File::write_all() will stop halfway.
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
warn!("error clearing flags on selection target fd: {err:?}");
}
if let Err(err) = File::from(fd).write_all(&buf) {
warn!("error writing selection: {err:?}");
}
@@ -198,7 +285,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();
}
@@ -273,7 +376,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;
};
@@ -288,11 +391,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();
}
@@ -347,6 +450,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();
}
}
@@ -360,12 +464,12 @@ impl ForeignToplevelHandler for State {
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !mapped
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
role.current
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
});
if !has_fullscreen_cap {
return;
}
@@ -375,7 +479,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);
}
}
@@ -393,14 +497,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);
@@ -483,3 +603,62 @@ 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() < XDG_ACTIVATION_TOKEN_TIMEOUT {
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();
self.niri.activation_state.remove_token(&token);
}
}
}
}
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);
+512 -143
View File
@@ -1,5 +1,8 @@
use std::cell::Cell;
use calloop::Interest;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
@@ -7,31 +10,42 @@ use smithay::input::pointer::Focus;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self};
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
use smithay::reexports::wayland_server::protocol::wl_output;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{self, Resource, WEnum};
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{
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::Layer;
use smithay::wayland::shell::wlr_layer::{self, Layer};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
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::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::layout::LayoutElement as _;
use crate::niri::{PopupGrabState, State};
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 {
@@ -47,25 +61,197 @@ impl XdgShellHandler for State {
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
self.unconstrain_popup(&surface);
let popup = PopupKind::Xdg(surface);
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking popup: {err:?}");
}
}
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().is::<DnDGrab<Self>>();
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().is::<DnDGrab<Self>>();
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);
}
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(
&mut self,
_surface: ToplevelSurface,
surface: ToplevelSurface,
_seat: WlSeat,
_serial: Serial,
_edges: ResizeEdge,
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
// FIXME
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();
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;
};
let edges = ResizeEdge::from(edges);
let window = mapped.window.clone();
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
last_cell.set(Some((time, edges)));
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
last_cell.set(None);
let intersection = edges.intersection(last_edges);
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.reset_window_height(Some(&window));
}
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
if !self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
return;
}
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
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(
@@ -79,7 +265,7 @@ impl XdgShellHandler for State {
state.geometry = geometry;
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
self.unconstrain_popup(&PopupKind::Xdg(surface.clone()));
surface.send_repositioned(token);
}
@@ -119,20 +305,25 @@ impl XdgShellHandler for State {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
&& layers.layers_on(Layer::Top).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
@@ -176,7 +367,7 @@ impl XdgShellHandler for State {
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
@@ -186,7 +377,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();
}
}
@@ -213,7 +404,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);
}
}
@@ -229,7 +420,7 @@ impl XdgShellHandler for State {
// The required configure will be the initial configure.
}
InitialConfigureState::Configured { output, .. } => {
InitialConfigureState::Configured { rules, output, .. } => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = requested_output
@@ -257,7 +448,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
@@ -268,7 +459,7 @@ impl XdgShellHandler for State {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None);
ws.configure_new_window(&unmapped.window, None, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -301,42 +492,56 @@ impl XdgShellHandler for State {
// The required configure will be the initial configure.
}
InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
..
workspace_name,
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
let mon = workspace_name
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name))
.map(|mon| (mon, false));
let mon = mon.or_else(|| {
output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
})
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
let ws = workspace_name
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
@@ -348,7 +553,7 @@ impl XdgShellHandler for State {
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width);
ws.configure_new_window(&unmapped.window, configure_width, rules);
}
// We already sent the initial configure, so we need to reconfigure.
@@ -386,19 +591,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| {
mapped.store_unmap_snapshot_if_empty(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();
@@ -447,7 +668,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();
}
}
@@ -460,17 +681,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);
@@ -481,18 +736,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");
@@ -503,8 +746,11 @@ impl State {
};
let config = self.niri.config.borrow();
let rules =
ResolvedWindowRules::compute(&config.window_rules, WindowRef::Unmapped(unmapped));
let rules = ResolvedWindowRules::compute(
&config.window_rules,
WindowRef::Unmapped(unmapped),
self.niri.is_at_startup,
);
let Unmapped { window, state } = unmapped;
@@ -513,12 +759,25 @@ impl State {
return;
};
// Pick the target monitor. First, check if we had an output set in the window rules.
// Pick the target monitor. First, check if we had a workspace set in the window rules.
let mon = rules
.open_on_output
.open_on_workspace
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o));
.and_then(|name| self.niri.layout.monitor_for_workspace(name));
// If not, check if we had an output set in the window rules.
let mon = mon.or_else(|| {
rules
.open_on_output
.as_deref()
.and_then(|name| {
self.niri
.global_space
.outputs()
.find(|output| output_matches_name(output, name))
})
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
@@ -551,16 +810,21 @@ 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;
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
let ws = rules
.open_on_workspace
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
@@ -579,7 +843,7 @@ impl State {
} else {
width
};
ws.configure_new_window(window, configure_width);
ws.configure_new_window(window, configure_width, &rules);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
@@ -599,6 +863,7 @@ impl State {
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name().cloned()),
};
toplevel.send_configure();
@@ -627,29 +892,23 @@ 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");
}
}
// Input method popups don't require a configure.
PopupKind::InputMethod(_) => (),
// Input method popup can arbitrary change its geometry, so we need to unconstrain
// it on commit.
PopupKind::InputMethod(_) => {
self.unconstrain_popup(&popup);
}
}
}
}
@@ -659,12 +918,12 @@ impl State {
self.niri.output_for_root(&root)
}
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
pub fn unconstrain_popup(&self, popup: &PopupKind) {
let _span = tracy_client::span!("Niri::unconstrain_popup");
// Popups with a NULL parent will get repositioned in their respective protocol handlers
// (i.e. layer-shell).
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
let Ok(root) = find_popup_root_surface(popup) else {
return;
};
@@ -680,7 +939,7 @@ impl State {
}
}
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
@@ -691,18 +950,16 @@ impl State {
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
target.loc.y -= self.niri.layout.window_y(window).unwrap();
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
target.loc -= get_popup_toplevel_coords(popup).to_f64();
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
self.position_popup_within_rect(popup, target);
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupSurface,
popup: &PopupKind,
layer_surface: &LayerSurface,
output: &Output,
) {
@@ -716,11 +973,47 @@ impl State {
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
target.loc -= get_popup_toplevel_coords(popup);
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
self.position_popup_within_rect(popup, target.to_f64());
}
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
match popup {
PopupKind::Xdg(popup) => {
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
PopupKind::InputMethod(popup) => {
let text_input_rectangle = popup.text_input_rectangle();
let mut bbox =
utils::bbox_from_surface_tree(popup.wl_surface(), text_input_rectangle.loc)
.to_f64();
// Position bbox horizontally first.
let overflow_x = (bbox.loc.x + bbox.size.w) - (target.loc.x + target.size.w);
if overflow_x > 0. {
bbox.loc.x -= overflow_x;
}
// Ensure that the popup starts within the window.
bbox.loc.x = f64::max(bbox.loc.x, target.loc.x);
// Try to position IME popup below the text input rectangle.
let mut below = bbox;
below.loc.y += f64::from(text_input_rectangle.size.h);
let mut above = bbox;
above.loc.y -= bbox.size.h;
if target.loc.y + target.size.h >= below.loc.y + below.size.h {
popup.set_location(below.loc.to_i32_round());
} else {
popup.set_location(above.loc.to_i32_round());
}
}
}
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
@@ -729,10 +1022,10 @@ impl State {
for (popup, _) in PopupManager::popups_for_surface(
window.toplevel().expect("no x11 support").wl_surface(),
) {
match popup {
PopupKind::Xdg(ref popup) => {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(popup, window, output);
self.unconstrain_window_popup(xdg_popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
@@ -748,8 +1041,11 @@ impl State {
let window_rules = &config.window_rules;
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
let new_rules =
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
let new_rules = ResolvedWindowRules::compute(
window_rules,
WindowRef::Unmapped(unmapped),
self.niri.is_at_startup,
);
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = new_rules;
}
@@ -758,11 +1054,11 @@ impl State {
.layout
.find_window_and_output_mut(toplevel.wl_surface())
{
if mapped.recompute_window_rules(window_rules) {
if mapped.recompute_window_rules(window_rules, self.niri.is_at_startup) {
drop(config);
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window);
self.niri.layout.update_window(&window, None);
if let Some(output) = output {
self.niri.queue_redraw(&output);
@@ -774,25 +1070,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.
@@ -804,27 +1100,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
@@ -833,32 +1140,94 @@ 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| {
mapped.store_unmap_snapshot_if_empty(renderer);
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
} else {
// The toplevel remains mapped; clear any stored unmap snapshot.
let _ = mapped.take_unmap_snapshot();
if animate {
state.backend.with_primary_renderer(|renderer| {
mapped.store_animation_snapshot(renderer);
});
let window = mapped.window.clone();
state.niri.layout.prepare_for_resize_animation(&window);
}
// The toplevel remains mapped; clear any stored unmap snapshot.
state.niri.layout.clear_unmap_snapshot(&window);
}
})
}
File diff suppressed because it is too large Load Diff
+224
View File
@@ -0,0 +1,224 @@
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
.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();
// FIXME: 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);
}
}
+175
View File
@@ -0,0 +1,175 @@
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct ResizeGrab {
start_data: PointerGrabStartData<State>,
window: Window,
}
impl ResizeGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self { start_data, window }
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for ResizeGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
let delta = event.location - self.start_data.location;
let ongoing = data
.niri
.layout
.interactive_resize_update(&self.window, delta);
if ongoing {
return;
}
}
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+230
View File
@@ -0,0 +1,230 @@
use std::time::Duration;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => None,
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(false, Some(false))
}
};
if let Some(output) = res {
state.niri.queue_redraw(&output);
}
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for SpatialMovementGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
let timestamp = Duration::from_millis(u64::from(event.time));
let delta = event.location - self.last_location;
self.last_location = event.location;
let layout = &mut data.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
layout.workspace_switch_gesture_update(-c.y, timestamp, false)
}
} else {
Some(None)
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_update(-delta.x, timestamp, false)
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_update(-delta.y, timestamp, false)
}
};
if let Some(output) = res {
if let Some(output) = output {
data.niri.queue_redraw(&output);
}
} else {
// The resize is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+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);
}
}
+322 -108
View File
@@ -1,5 +1,10 @@
use anyhow::{anyhow, bail, Context};
use niri_ipc::{LogicalOutput, Mode, Output, Request, Response, Socket, Transform};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
};
use serde_json::json;
use crate::cli::Msg;
@@ -10,13 +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")?;
@@ -27,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,
};
@@ -106,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!();
}
}
@@ -215,29 +144,314 @@ 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:?}");
};
}
Msg::Output { output, .. } => {
let Response::OutputConfigChanged(response) = response else {
bail!("unexpected response: expected OutputConfigChanged, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response == OutputConfigChanged::OutputWasMissing {
println!("Output \"{output}\" is not connected.");
println!("The change will apply when it is connected.");
}
}
Msg::Workspaces => {
let Response::Workspaces(mut response) = response else {
bail!("unexpected response: expected Workspaces, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
if response.is_empty() {
println!("No workspaces.");
return Ok(());
}
response.sort_by_key(|ws| ws.idx);
response.sort_by(|a, b| a.output.cmp(&b.output));
let mut current_output = if let Some(output) = response[0].output.as_deref() {
println!("Output \"{output}\":");
Some(output)
} else {
println!("No output:");
None
};
for ws in &response {
if ws.output.as_deref() != current_output {
let output = ws.output.as_deref().context(
"invalid response: workspace with no output \
following a workspace with an output",
)?;
current_output = Some(output);
println!("\nOutput \"{output}\":");
}
let is_active = if ws.is_active { " * " } else { " " };
let idx = ws.idx;
let name = if let Some(name) = ws.name.as_deref() {
format!(" \"{name}\"")
} else {
String::new()
};
println!("{is_active}{idx}{name}");
}
}
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)");
}
}
+444 -31
View File
@@ -1,33 +1,58 @@
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::{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;
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::utils::{version, with_toplevel_role};
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 +84,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 +141,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 +161,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,8 +175,12 @@ 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 = request.and_then(|request| process(&ctx, request));
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
if let Err(err) = &reply {
if !requested_error {
@@ -128,48 +188,401 @@ 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(())
}
fn process(ctx: &ClientCtx, request: Request) -> Reply {
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let response = match request {
Request::ReturnError => return Err(String::from("example compositor error")),
Request::Version => Response::Version(version()),
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
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) => {
let (tx, rx) = async_channel::bounded(1);
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action, false);
let _ = tx.send_blocking(());
});
// Wait until the action has been processed before returning. This is important for a
// few actions, for instance for DoScreenTransition this wait ensures that the screen
// contents were sampled into the texture.
let _ = rx.recv().await;
Response::Handled
}
Request::Output { output, action } => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.values()
.any(|o| OutputName::from_ipc_output(o).matches(&output));
let response = if found {
OutputConfigChanged::Applied
} else {
OutputConfigChanged::OutputWasMissing
};
drop(ipc_outputs);
ctx.event_loop.insert_idle(move |state| {
state.apply_transient_output_config(&output, action);
});
Response::OutputConfigChanged(response)
}
Request::FocusedOutput => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let active_output = state
.niri
.layout
.active_output()
.map(|output| output.name());
let output = active_output.and_then(|active_output| {
state
.backend
.ipc_outputs()
.lock()
.unwrap()
.values()
.find(|o| o.name == active_output)
.cloned()
});
let _ = tx.send_blocking(output);
});
let result = rx.recv().await;
let output = result.map_err(|_| String::from("error getting active output info"))?;
Response::FocusedOutput(output)
}
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 {
with_toplevel_role(mapped.toplevel(), |role| 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;
changed |= with_toplevel_role(mapped.toplevel(), |role| {
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);
}
}
}
+162 -49
View File
@@ -1,21 +1,28 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Logical, Point, Scale, Transform};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
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;
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 {
@@ -28,116 +35,221 @@ pub struct ClosingWindow {
/// Where the window should be blocked out from.
block_out_from: Option<BlockOutFrom>,
/// Center of the window geometry.
center: Point<i32, Logical>,
/// Size of the window geometry.
geo_size: Size<f64, Logical>,
/// Position in the workspace.
pos: Point<i32, Logical>,
pos: Point<f64, Logical>,
/// How much the buffer should be offset.
buffer_offset: Point<i32, Logical>,
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the blocked-out buffer should be offset.
blocked_out_buffer_offset: Point<i32, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
/// The closing animation.
anim: Animation,
anim_state: AnimationState,
/// Alpha the animation should start from.
starting_alpha: f32,
/// Scale the animation should start from.
starting_scale: f64,
/// Random seed for the shader.
random_seed: f32,
}
niri_render_elements! {
ClosingWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
#[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 {
#[allow(clippy::too_many_arguments)]
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: i32,
center: Point<i32, Logical>,
pos: Point<i32, Logical>,
scale: Scale<f64>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
blocker: TransactionBlocker,
anim: Animation,
starting_alpha: f32,
starting_scale: f64,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
let mut render_to_buffer = |elements: Vec<E>| -> anyhow::Result<_> {
let mut render_to_texture = |elements: Vec<E>| -> anyhow::Result<_> {
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
Scale::from(scale as f64),
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
)
.context("error rendering to texture")?;
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let offset = geo.loc.to_logical(scale);
let buffer = TextureBuffer::from_texture(
renderer,
texture,
scale,
Transform::Normal,
Vec::new(),
);
let offset = geo.loc.to_f64().to_logical(scale);
Ok((buffer, offset))
};
let (buffer, buffer_offset) =
render_to_buffer(snapshot.contents).context("error rendering contents")?;
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_buffer(snapshot.blocked_out_contents)
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
center,
geo_size,
pos,
buffer_offset,
blocked_out_buffer_offset,
anim,
starting_alpha,
starting_scale,
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_clamped_done()
match &self.anim_state {
AnimationState::Waiting { .. } => true,
AnimationState::Animating(anim) => !anim.is_done(),
}
}
pub fn render(
&self,
view_pos: i32,
renderer: &mut GlesRenderer,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let val = self.anim.clamped_value();
let block_out = match self.block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
};
let (buffer, offset) = if block_out {
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.buffer, self.buffer_offset)
};
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);
// Round to physical pixels relative to the view position. This is similar to what
// happens when rendering normal windows.
let relative = self.pos - view_rect.loc;
let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale);
let geo_loc = Vec2::new(pos.x as f32, pos.y as f32);
let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = self.buffer.texture_scale();
let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = self.buffer.texture().size();
let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return ShaderRenderElement::new(
ProgramType::Close,
view_rect.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
Kind::Unspecified,
)
.with_location(Point::from((0., 0.)))
.into();
}
let elem = TextureRenderElement::from_texture_buffer(
buffer.clone(),
Point::from((0., 0.)),
buffer,
Some(val.clamp(0., 1.) as f32 * self.starting_alpha),
1. - clamped_progress as f32,
None,
None,
Kind::Unspecified,
@@ -145,14 +257,15 @@ impl ClosingWindow {
let elem = PrimaryGpuTextureRenderElement(elem);
let center = self.geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(self.center - offset).to_physical_precise_round(scale),
((val / 5. + 0.8) * self.starting_scale).max(0.),
(center - offset).to_physical_precise_round(scale),
((1. - clamped_progress) / 5. + 0.8).max(0.),
);
let mut location = self.pos + offset;
location.x -= view_pos;
location.x -= view_rect.loc.x;
let elem = RelocateRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
+192 -88
View File
@@ -1,30 +1,31 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::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::gradient::GradientRenderElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
sizes: [Size<i32, Logical>; 4],
full_size: Size<i32, Logical>,
is_active: bool,
buffers: [SolidColorBuffer; 8],
locations: [Point<f64, Logical>; 8],
sizes: [Size<f64, Logical>; 8],
borders: [BorderRenderElement; 8],
full_size: Size<f64, Logical>,
is_border: bool,
use_border_shader: bool,
config: niri_config::FocusRing,
}
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = GradientRenderElement,
Gradient = BorderRenderElement,
}
}
@@ -34,9 +35,10 @@ impl FocusRing {
buffers: Default::default(),
locations: Default::default(),
sizes: Default::default(),
borders: Default::default(),
full_size: Default::default(),
is_active: false,
is_border: false,
use_border_shader: false,
config,
}
}
@@ -45,117 +47,219 @@ impl FocusRing {
self.config = config;
}
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
if is_border {
self.sizes[0] = Size::from((win_size.w + width * 2, width));
self.sizes[1] = Size::from((win_size.w + width * 2, width));
self.sizes[2] = Size::from((width, win_size.h));
self.sizes[3] = Size::from((width, win_size.h));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
pub fn update_shaders(&mut self) {
for elem in &mut self.borders {
elem.damage_all();
}
self.is_border = is_border;
}
pub fn set_active(&mut self, is_active: bool) {
pub fn update_render_elements(
&mut self,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
self.config.active_color.into()
self.config.active_color
} else {
self.config.inactive_color.into()
self.config.inactive_color
};
for buf in &mut self.buffers {
buf.set_color(color);
buf.set_color(color.to_array_premul());
}
self.is_active = is_active;
}
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.config.off {
return rv.into_iter();
}
let gradient = if self.is_active {
let gradient = if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
let elem = gradient.and_then(|gradient| {
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
GradientRenderElement::new(
renderer,
scale,
Rectangle::from_loc_and_size(location, size),
gradient_area,
gradient.from.into(),
gradient.to.into(),
// Set the defaults for solid color + rounded corners.
let gradient = gradient.unwrap_or(Gradient {
from: color,
to: color,
angle: 0,
relative_to: GradientRelativeTo::Window,
in_: GradientInterpolation::default(),
});
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
let rounded_corner_border_width = if self.is_border {
// HACK: increase the border width used for the inner rounded corners a tiny bit to
// reduce background bleed.
width as f32 + 0.5
} else {
0.
};
let ceil = |logical: f64| (logical * scale).ceil() / scale;
// All of this stuff should end up aligned to physical pixels because:
// * Window size and border width are rounded to physical pixels before being passed to this
// function.
// * We will ceil the corner radii below.
// * We do not divide anything, only add, subtract and multiply by integers.
// * At rendering time, tile positions are rounded to physical pixels.
if is_border {
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
let top_right = f64::min(
self.full_size.w - top_left,
f64::max(width, ceil(f64::from(radius.top_right))),
);
let bottom_left = f64::min(
self.full_size.h - top_left,
f64::max(width, ceil(f64::from(radius.bottom_left))),
);
let bottom_right = f64::min(
self.full_size.h - top_right,
f64::min(
self.full_size.w - bottom_left,
f64::max(width, ceil(f64::from(radius.bottom_right))),
),
);
// Top edge.
self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width));
self.locations[0] = Point::from((-width + top_left, -width));
// Bottom edge.
self.sizes[1] =
Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width));
self.locations[1] = Point::from((-width + bottom_left, win_size.h));
// Left edge.
self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left));
self.locations[2] = Point::from((-width, -width + top_left));
// Right edge.
self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right));
self.locations[3] = Point::from((win_size.w, -width + top_right));
// Top-left corner.
self.sizes[4] = Size::from((top_left, top_left));
self.locations[4] = Point::from((-width, -width));
// Top-right corner.
self.sizes[5] = Size::from((top_right, top_right));
self.locations[5] = Point::from((win_size.w + width - top_right, -width));
// Bottom-right corner.
self.sizes[6] = Size::from((bottom_right, bottom_right));
self.locations[6] = Point::from((
win_size.w + width - bottom_right,
win_size.h + width - bottom_right,
));
// Bottom-left corner.
self.sizes[7] = Size::from((bottom_left, bottom_left));
self.locations[7] = Point::from((-width, win_size.h + width - bottom_left));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
border.update(
size,
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
)
.map(Into::into)
});
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
let elem = elem.unwrap_or_else(|| {
SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
self.borders[0].update(
self.sizes[0],
Rectangle::from_loc_and_size(
gradient_area.loc - self.locations[0],
gradient_area.size,
),
gradient.in_,
gradient.from,
gradient.to,
((gradient.angle as f32) - 90.).to_radians(),
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
rounded_corner_border_width,
radius,
scale as f32,
);
}
self.is_border = is_border;
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 8>::new();
if self.config.off {
return rv.into_iter();
}
let border_width = -self.locations[0].y;
// If drawing as a border with width = 0, then there's nothing to draw.
if self.is_border && border_width == 0. {
return rv.into_iter();
}
let has_border_shader = BorderRenderElement::has_shader(renderer);
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
let elem = if self.use_border_shader && has_border_shader {
border.clone().with_location(location).into()
} else {
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
};
rv.push(elem);
};
if self.is_border {
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
push(buf, location + loc, size);
for ((buf, border), loc) in zip(zip(&self.buffers, &self.borders), self.locations) {
push(buf, border, location + loc);
}
} else {
push(
&self.buffers[0],
&self.borders[0],
location + self.locations[0],
self.sizes[0],
);
}
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 {
+61
View File
@@ -0,0 +1,61 @@
use niri_config::{CornerRadius, FloatOrInt};
use smithay::utils::{Logical, Point, Rectangle, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct InsertHintElement {
inner: FocusRing,
}
pub type InsertHintRenderElement = FocusRingRenderElement;
impl InsertHintElement {
pub fn new(config: niri_config::InsertHint) -> Self {
Self {
inner: FocusRing::new(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
}),
}
}
pub fn update_config(&mut self, config: niri_config::InsertHint) {
self.inner.update_config(niri_config::FocusRing {
off: config.off,
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
});
}
pub fn update_shaders(&mut self) {
self.inner.update_shaders();
}
pub fn update_render_elements(
&mut self,
size: Size<f64, Logical>,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
) {
self.inner
.update_render_elements(size, true, false, view_rect, radius, scale);
}
pub fn render(
&self,
renderer: &mut impl NiriRenderer,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
self.inner.render(renderer, location)
}
}
+3017 -336
View File
File diff suppressed because it is too large Load Diff
+426 -205
View File
@@ -7,19 +7,21 @@ 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,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::input::swipe_tracker::SwipeTracker;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::output_size;
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,14 +106,50 @@ 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]
}
pub fn find_named_workspace(&self, workspace_name: &str) -> Option<&Workspace<W>> {
self.workspaces.iter().find(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
pub fn find_named_workspace_index(&self, workspace_name: &str) -> Option<usize> {
self.workspaces.iter().position(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
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;
@@ -141,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);
@@ -175,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);
@@ -196,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());
@@ -204,7 +300,7 @@ impl<W: LayoutElement> Monitor<W> {
continue;
}
if !self.workspaces[idx].has_windows() {
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
@@ -213,6 +309,20 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn unname_workspace(&mut self, workspace_name: &str) -> bool {
for ws in &mut self.workspaces {
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
{
ws.unname();
return true;
}
}
false
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}
@@ -266,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();
}
@@ -290,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();
}
@@ -298,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() {
@@ -343,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) {
@@ -366,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 {
@@ -384,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) {
@@ -416,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);
}
@@ -433,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);
}
@@ -450,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) {
@@ -477,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) {
@@ -523,7 +719,7 @@ impl<W: LayoutElement> Monitor<W> {
Some(column.tiles[column.active_tile_idx].window())
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
@@ -533,11 +729,11 @@ impl<W: LayoutElement> Monitor<W> {
}
for ws in &mut self.workspaces {
ws.advance_animations(current_time, is_active);
ws.advance_animations(current_time);
}
}
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())
@@ -552,17 +748,48 @@ impl<W: LayoutElement> Monitor<W> {
.any(|ws| ws.are_transitions_ongoing())
}
pub fn update_render_elements(&mut self, is_active: bool) {
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return;
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
self.workspaces[after_idx].update_render_elements(is_active);
if before_idx < 0. {
return;
}
}
let before_idx = before_idx as usize;
self.workspaces[before_idx].update_render_elements(is_active);
}
None => {
self.workspaces[self.active_workspace_idx].update_render_elements(is_active);
}
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
for ws in &mut self.workspaces {
ws.update_config(options.clone());
}
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);
}
}
@@ -581,10 +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 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 {
@@ -632,65 +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 window_under(
pub fn workspaces_with_render_positions(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
let mut first = None;
let mut second = None;
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 < 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);
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
// 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));
}
}
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)
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<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> {
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 {
@@ -703,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
@@ -816,6 +1012,7 @@ impl<W: LayoutElement> Monitor<W> {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
is_touchpad,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
@@ -824,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;
@@ -846,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;
+154
View File
@@ -0,0 +1,154 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Context as _;
use glam::{Mat3, Vec2};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::utils::{
Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::render_to_encompassing_texture;
use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
#[derive(Debug)]
pub struct OpenAnimation {
anim: Animation,
random_seed: f32,
}
niri_render_elements! {
OpeningWindowRenderElement => {
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
Shader = ShaderRenderElement,
}
}
impl OpenAnimation {
pub fn new(anim: Animation) -> Self {
Self {
anim,
random_seed: fastrand::f32(),
}
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
}
pub fn is_done(&self) -> bool {
self.anim.is_done()
}
// We can't depend on view_rect here, because the result of window opening can be snapshot and
// then rendered elsewhere.
pub fn render(
&self,
renderer: &mut GlesRenderer,
elements: &[impl RenderElement<GlesRenderer>],
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (texture, _sync_point, geo) = render_to_encompassing_texture(
renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
elements,
)
.context("error rendering to texture")?;
let offset = geo.loc.to_f64().to_logical(scale);
let texture_size = geo.size.to_f64().to_logical(scale);
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
// Expand the area a bit to allow for more varied effects.
let mut target_size = area.size.upscale(1.5);
target_size.w = f64::max(area.size.w + 1000., target_size.w);
target_size.h = f64::max(area.size.h + 1000., target_size.h);
let diff = (target_size.to_point() - area.size.to_point()).downscale(2.);
let diff = diff.to_physical_precise_round(scale).to_logical(scale);
area.loc -= diff;
area.size += diff.upscale(2.).to_size();
let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32);
let area_size = Vec2::new(area.size.w as f32, area.size.h as f32);
let geo_loc = Vec2::new(location.x as f32, location.y as f32);
let geo_size = Vec2::new(geo_size.w as f32, geo_size.h as f32);
let input_to_geo = Mat3::from_scale(area_size / geo_size)
* Mat3::from_translation((area_loc - geo_loc) / area_size);
let tex_scale = Vec2::new(scale.x as f32, scale.y as f32);
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale;
let geo_to_tex =
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
return Ok(ShaderRenderElement::new(
ProgramType::Open,
area.size,
None,
scale.x as f32,
1.,
vec![
mat3_uniform("niri_input_to_geo", input_to_geo),
Uniform::new("niri_geo_size", geo_size.to_array()),
mat3_uniform("niri_geo_to_tex", geo_to_tex),
Uniform::new("niri_progress", progress as f32),
Uniform::new("niri_clamped_progress", clamped_progress as f32),
Uniform::new("niri_random_seed", self.random_seed),
],
HashMap::from([(String::from("niri_tex"), texture.clone())]),
Kind::Unspecified,
)
.with_location(area.loc)
.into());
}
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
let elem = TextureRenderElement::from_texture_buffer(
buffer,
Point::from((0., 0.)),
clamped_progress as f32,
None,
None,
Kind::Unspecified,
);
let elem = PrimaryGpuTextureRenderElement(elem);
let center = geo_size.to_point().downscale(2.);
let elem = RescaleRenderElement::from_element(
elem,
(center - offset).to_physical_precise_round(scale),
(progress / 2. + 0.5).max(0.),
);
let elem = RelocateRenderElement::from_element(
elem,
(location + offset).to_physical_precise_round(scale),
Relocate::Relative,
);
Ok(elem.into())
}
}
+448 -285
View File
File diff suppressed because it is too large Load Diff
+2457 -1073
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -16,8 +16,6 @@ pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod rubber_band;
pub mod scroll_tracker;
pub mod swipe_tracker;
pub mod ui;
pub mod utils;
pub mod window;
+105 -63
View File
@@ -18,17 +18,26 @@ 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;
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
#[cfg(feature = "profile-with-tracy-allocations")]
#[global_allocator]
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
if env::var_os("RUST_BACKTRACE").is_none() {
@@ -50,7 +59,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
@@ -78,8 +87,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("XDG_SESSION_TYPE", "wayland");
}
let _client = tracy_client::Client::start();
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
@@ -87,9 +94,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
let path = config
.or_else(default_config_path)
.expect("error getting config path");
tracy_client::Client::start();
let (path, _, _) = config_path(config);
Config::load(&path)?;
info!("config is valid");
return Ok(());
@@ -102,58 +109,58 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Avoid starting Tracy for the `niri msg` code path since starting/stopping Tracy is a bit
// slow.
tracy_client::Client::start();
info!("starting version {}", &version());
// Load the config.
let 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();
@@ -167,6 +174,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();
@@ -188,7 +197,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());
}
@@ -208,30 +217,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.
@@ -261,7 +270,7 @@ fn import_environment() {
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
SOCKET_PATH_ENV,
]
.join(" ");
@@ -306,6 +315,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");
@@ -317,6 +332,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()?,
+1696 -434
View File
File diff suppressed because it is too large Load Diff
+11 -27
View File
@@ -11,10 +11,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
@@ -22,6 +19,7 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
use crate::utils::with_toplevel_role;
const VERSION: u32 = 3;
@@ -95,38 +93,24 @@ 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| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
state.niri.layout.with_windows(|mapped, output, _| {
let toplevel = mapped.toplevel();
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
refresh_toplevel(protocol_state, wl_surface, role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
let toplevel = window.toplevel().expect("no X11 support");
let wl_surface = toplevel.wl_surface();
with_toplevel_role(toplevel, |role| {
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
});
}
}
+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();
}
}
}
}
+747 -108
View File
File diff suppressed because it is too large Load Diff
+301
View File
@@ -0,0 +1,301 @@
use std::collections::HashMap;
use glam::{Mat3, Vec2};
use niri_config::{
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::renderer::NiriRenderer;
use super::shader_element::ShaderRenderElement;
use super::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a wide variety of borders and border parts.
///
/// This includes:
/// * sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
/// * corner rounding.
/// * as a background rectangle and as parts of a border line.
#[derive(Debug, Clone)]
pub struct BorderRenderElement {
inner: ShaderRenderElement,
params: Parameters,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Parameters {
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
}
impl BorderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
) -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
let mut rv = Self {
inner,
params: Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
},
};
rv.update_inner();
rv
}
pub fn empty() -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
Self {
inner,
params: Parameters {
size: Default::default(),
gradient_area: Default::default(),
gradient_format: GradientInterpolation::default(),
color_from: Default::default(),
color_to: Default::default(),
angle: 0.,
geometry: Default::default(),
border_width: 0.,
corner_radius: Default::default(),
scale: 1.,
},
}
}
pub fn damage_all(&mut self) {
self.inner.damage_all();
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
gradient_format: GradientInterpolation,
color_from: Color,
color_to: Color,
angle: f32,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
scale: f32,
) {
let params = Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
};
if self.params == params {
return;
}
self.params = params;
self.update_inner();
}
fn update_inner(&mut self) {
let Parameters {
size,
gradient_area,
gradient_format,
color_from,
color_to,
angle,
geometry,
border_width,
corner_radius,
scale,
} = self.params;
let grad_offset = geometry.loc - gradient_area.loc;
let grad_offset = Vec2::new(grad_offset.x as f32, grad_offset.y as f32);
let grad_dir = Vec2::from_angle(angle);
let (w, h) = (gradient_area.size.w as f32, gradient_area.size.h as f32);
let mut grad_area_diag = Vec2::new(w, h);
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
grad_area_diag.x = -w;
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y < 0. {
grad_vec = -grad_vec;
}
let area_size = Vec2::new(size.w as f32, size.h as f32);
let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32);
let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32);
let input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
let colorspace = match gradient_format.color_space {
GradientColorSpace::Srgb => 0.,
GradientColorSpace::SrgbLinear => 1.,
GradientColorSpace::Oklab => 2.,
GradientColorSpace::Oklch => 3.,
};
let hue_interpolation = match gradient_format.hue_interpolation {
HueInterpolation::Shorter => 0.,
HueInterpolation::Longer => 1.,
HueInterpolation::Increasing => 2.,
HueInterpolation::Decreasing => 3.,
};
self.inner.update(
size,
None,
scale,
vec![
Uniform::new("colorspace", colorspace),
Uniform::new("hue_interpolation", hue_interpolation),
Uniform::new("color_from", color_from.to_array_unpremul()),
Uniform::new("color_to", color_to.to_array_unpremul()),
Uniform::new("grad_offset", grad_offset.to_array()),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
mat3_uniform("input_to_geo", input_to_geo),
Uniform::new("geo_size", geo_size.to_array()),
Uniform::new("outer_radius", <[f32; 4]>::from(corner_radius)),
Uniform::new("border_width", border_width),
],
HashMap::new(),
);
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Border)
.is_some()
}
}
impl Default for BorderRenderElement {
fn default() -> Self {
Self::empty()
}
}
impl Element for BorderRenderElement {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.inner.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.inner.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for BorderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
+302
View File
@@ -0,0 +1,302 @@
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::damage::ExtraDamage;
use super::renderer::{AsGlesFrame as _, NiriRenderer};
use super::shaders::{mat3_uniform, Shaders};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
#[derive(Debug)]
pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
inner: WaylandSurfaceRenderElement<R>,
program: GlesTexProgram,
corner_radius: CornerRadius,
geometry: Rectangle<f64, Logical>,
input_to_geo: Mat3,
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
}
#[derive(Debug, Default, Clone)]
pub struct RoundedCornerDamage {
damage: ExtraDamage,
corner_radius: CornerRadius,
}
impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn new(
elem: WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<f64, Logical>,
program: GlesTexProgram,
corner_radius: CornerRadius,
) -> Self {
let elem_geo = elem.geometry(scale);
let elem_geo_loc = Vec2::new(elem_geo.loc.x as f32, elem_geo.loc.y as f32);
let elem_geo_size = Vec2::new(elem_geo.size.w as f32, elem_geo.size.h as f32);
let geo = geometry.to_physical_precise_round(scale);
let geo_loc = Vec2::new(geo.loc.x, geo.loc.y);
let geo_size = Vec2::new(geo.size.w, geo.size.h);
let buf_size = elem.buffer_size();
let buf_size = Vec2::new(buf_size.w as f32, buf_size.h as f32);
let view = elem.view();
let src_loc = Vec2::new(view.src.loc.x as f32, view.src.loc.y as f32);
let src_size = Vec2::new(view.src.size.w as f32, view.src.size.h as f32);
let transform = elem.transform();
// HACK: ??? for some reason flipped ones are fine.
let transform = match transform {
Transform::_90 => Transform::_270,
Transform::_270 => Transform::_90,
x => x,
};
let transform_matrix = Mat3::from_translation(Vec2::new(0.5, 0.5))
* Mat3::from_cols_array(transform.matrix().as_ref())
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
// FIXME: y_inverted
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
// Apply viewporter src.
* Mat3::from_scale(buf_size / src_size)
* Mat3::from_translation(-src_loc / buf_size);
Self {
inner: elem,
program,
corner_radius,
geometry,
input_to_geo,
scale: scale.x as f32,
}
}
pub fn shader(renderer: &mut R) -> Option<&GlesTexProgram> {
Shaders::get(renderer).clipped_surface.as_ref()
}
pub fn will_clip(
elem: &WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> bool {
let elem_geo = elem.geometry(scale);
let geo = geometry.to_physical_precise_round(scale);
if corner_radius == CornerRadius::default() {
!geo.contains_rect(elem_geo)
} else {
let corners = Self::rounded_corners(geometry, corner_radius);
let corners = corners
.into_iter()
.map(|rect| rect.to_physical_precise_up(scale));
let geo = Rectangle::subtract_rects_many([geo], corners);
!Rectangle::subtract_rects_many([elem_geo], geo).is_empty()
}
}
fn rounded_corners(
geo: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> [Rectangle<f64, Logical>; 4] {
let top_left = corner_radius.top_left as f64;
let top_right = corner_radius.top_right as f64;
let bottom_right = corner_radius.bottom_right as f64;
let bottom_left = corner_radius.bottom_left as f64;
[
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
Rectangle::from_loc_and_size(
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
(top_right, top_right),
),
Rectangle::from_loc_and_size(
(
geo.loc.x + geo.size.w - bottom_right,
geo.loc.y + geo.size.h - bottom_right,
),
(bottom_right, bottom_right),
),
Rectangle::from_loc_and_size(
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
(bottom_left, bottom_left),
),
]
}
}
impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
// FIXME: radius changes need to cause damage.
let damage = self.inner.damage_since(scale, commit);
// Intersect with geometry, since we're clipping by it.
let mut geo = self.geometry.to_physical_precise_round(scale);
geo.loc -= self.geometry(scale).loc;
damage
.into_iter()
.filter_map(|rect| rect.intersection(geo))
.collect()
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
let regions = self.inner.opaque_regions(scale);
// Intersect with geometry, since we're clipping by it.
let mut geo = self.geometry.to_physical_precise_round(scale);
geo.loc -= self.geometry(scale).loc;
let regions = regions
.into_iter()
.filter_map(|rect| rect.intersection(geo));
// Subtract the rounded corners.
if self.corner_radius == CornerRadius::default() {
regions.collect()
} else {
let corners = Self::rounded_corners(self.geometry, self.corner_radius);
let elem_loc = self.geometry(scale).loc;
let corners = corners.into_iter().map(|rect| {
let mut rect = rect.to_physical_precise_up(scale);
rect.loc -= elem_loc;
rect
});
OpaqueRegions::from_slice(&Rectangle::subtract_rects_many(regions, corners))
}
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
frame.override_default_tex_program(
self.program.clone(),
vec![
Uniform::new("niri_scale", self.scale),
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.clear_tex_program_override();
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>>
for ClippedSurfaceRenderElement<TtyRenderer<'render>>
{
fn draw(
&self,
frame: &mut TtyFrame<'render, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
frame.as_gles_frame().override_default_tex_program(
self.program.clone(),
vec![
Uniform::new(
"geo_size",
(self.geometry.size.w as f32, self.geometry.size.h as f32),
),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_geo),
],
);
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
frame.as_gles_frame().clear_tex_program_override();
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl RoundedCornerDamage {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
self.damage.set_size(size);
}
pub fn set_corner_radius(&mut self, corner_radius: CornerRadius) {
if self.corner_radius == corner_radius {
return;
}
// FIXME: make the damage granular.
self.corner_radius = corner_radius;
self.damage.damage_all();
}
pub fn element(&self) -> ExtraDamage {
self.damage.clone()
}
}
-162
View File
@@ -1,162 +0,0 @@
use std::collections::HashMap;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::primary_gpu_pixel_shader_with_textures::PrimaryGpuPixelShaderWithTexturesRenderElement;
use super::renderer::AsGlesFrame;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
#[derive(Debug)]
pub struct CrossfadeRenderElement(PrimaryGpuPixelShaderWithTexturesRenderElement);
impl CrossfadeRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
renderer: &mut GlesRenderer,
area: Rectangle<i32, Logical>,
scale: Scale<f64>,
texture_from: (GlesTexture, Rectangle<i32, Physical>),
size_from: Size<i32, Logical>,
texture_to: (GlesTexture, Rectangle<i32, Physical>),
size_to: Size<i32, Logical>,
amount: f32,
result_alpha: f32,
) -> Option<Self> {
let (texture_from, texture_from_geo) = texture_from;
let (texture_to, texture_to_geo) = texture_to;
let scale_from = area.size.to_f64() / size_from.to_f64();
let scale_to = area.size.to_f64() / size_to.to_f64();
let tex_from_geo = texture_from_geo.to_f64().upscale(scale_from);
let tex_to_geo = texture_to_geo.to_f64().upscale(scale_to);
let combined_geo = tex_from_geo.merge(tex_to_geo);
let size = combined_geo
.size
.to_logical(1.)
.to_buffer(1., Transform::Normal);
let area = Rectangle::from_loc_and_size(
area.loc + combined_geo.loc.to_logical(scale).to_i32_round(),
combined_geo.size.to_logical(scale).to_i32_round(),
);
let tex_from_loc = (tex_from_geo.loc - combined_geo.loc)
.downscale((combined_geo.size.w, combined_geo.size.h));
let tex_to_loc = (tex_to_geo.loc - combined_geo.loc)
.downscale((combined_geo.size.w, combined_geo.size.h));
let tex_from_size = tex_from_geo.size / combined_geo.size;
let tex_to_size = tex_to_geo.size / combined_geo.size;
// FIXME: cropping this element will mess up the coordinates.
Shaders::get(renderer).crossfade.clone().map(|shader| {
Self(PrimaryGpuPixelShaderWithTexturesRenderElement::new(
shader,
HashMap::from([
(String::from("tex_from"), texture_from),
(String::from("tex_to"), texture_to),
]),
area,
size,
None,
result_alpha,
vec![
Uniform::new(
"tex_from_loc",
(tex_from_loc.x as f32, tex_from_loc.y as f32),
),
Uniform::new(
"tex_from_size",
(tex_from_size.x as f32, tex_from_size.y as f32),
),
Uniform::new("tex_to_loc", (tex_to_loc.x as f32, tex_to_loc.y as f32)),
Uniform::new("tex_to_size", (tex_to_size.x as f32, tex_to_size.y as f32)),
Uniform::new("amount", amount),
],
Kind::Unspecified,
))
})
}
}
impl Element for CrossfadeRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for CrossfadeRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for CrossfadeRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+76
View File
@@ -0,0 +1,76 @@
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::Renderer;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
#[derive(Debug, Clone)]
pub struct ExtraDamage {
id: Id,
commit: CommitCounter,
geometry: Rectangle<f64, Logical>,
}
impl ExtraDamage {
pub fn new() -> Self {
Self {
id: Id::new(),
commit: Default::default(),
geometry: Default::default(),
}
}
pub fn set_size(&mut self, size: Size<f64, Logical>) {
if self.geometry.size == size {
return;
}
self.geometry.size = size;
self.commit.increment();
}
pub fn damage_all(&mut self) {
self.commit.increment();
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.geometry.loc = location;
self
}
}
impl Default for ExtraDamage {
fn default() -> Self {
Self::new()
}
}
impl Element for ExtraDamage {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_loc_and_size((0., 0.), (1., 1.))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_up(scale)
}
}
impl<R: Renderer> RenderElement<R> for ExtraDamage {
fn draw(
&self,
_frame: &mut <R as Renderer>::Frame<'_>,
_src: Rectangle<f64, Buffer>,
_dst: Rectangle<i32, Physical>,
_damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), R::Error> {
Ok(())
}
}
+81
View File
@@ -0,0 +1,81 @@
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::Scale;
use super::renderer::NiriRenderer;
use crate::niri::OutputRenderElements;
pub fn draw_opaque_regions<R: NiriRenderer>(
elements: &mut Vec<OutputRenderElements<R>>,
scale: Scale<f64>,
) {
let _span = tracy_client::span!("draw_opaque_regions");
let mut i = 0;
while i < elements.len() {
let elem = &elements[i];
i += 1;
// HACK
if format!("{elem:?}").contains("ExtraDamage") {
continue;
}
let geo = elem.geometry(scale);
let mut opaque = elem.opaque_regions(scale).to_vec();
for rect in &mut opaque {
rect.loc += geo.loc;
}
let semitransparent = geo.subtract_rects(opaque.iter().copied());
for rect in opaque {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
CommitCounter::default(),
[0., 0., 0.2, 0.2],
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
i += 1;
}
for rect in semitransparent {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
i += 1;
}
}
}
pub fn draw_damage<R: NiriRenderer>(
damage_tracker: &mut OutputDamageTracker,
elements: &mut Vec<OutputRenderElements<R>>,
) {
let _span = tracy_client::span!("draw_damage");
let Ok((Some(damage), _)) = damage_tracker.damage_output(1, elements) else {
return;
};
for rect in damage {
let color = SolidColorRenderElement::new(
Id::new(),
*rect,
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Kind::Unspecified,
);
elements.insert(0, OutputRenderElements::SolidColor(color));
}
}
-135
View File
@@ -1,135 +0,0 @@
use glam::Vec2;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
use super::renderer::NiriRenderer;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
#[derive(Debug)]
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
impl GradientRenderElement {
pub fn new(
renderer: &mut impl NiriRenderer,
scale: Scale<f64>,
area: Rectangle<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
) -> Option<Self> {
let shader = Shaders::get(renderer).gradient_border.clone()?;
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
let grad_dir = Vec2::from_angle(angle);
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
let mut grad_area_diag = Vec2::new(w, h);
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
grad_area_diag.x = -w;
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y <= 0. {
grad_vec = -grad_vec;
}
let elem = PixelShaderElement::new(
shader,
area,
None,
1.,
vec![
Uniform::new("color_from", color_from),
Uniform::new("color_to", color_to),
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
],
Kind::Unspecified,
);
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
}
}
impl Element for GradientRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for GradientRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+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)
}
}
+90 -45
View File
@@ -1,33 +1,40 @@
use std::ptr;
use anyhow::{ensure, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::{Buffer, Fourcc};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{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 crossfade;
pub mod gradient;
pub mod border;
pub mod clipped_surface;
pub mod damage;
pub mod debug;
pub mod memory;
pub mod offscreen;
pub mod primary_gpu_pixel_shader;
pub mod primary_gpu_pixel_shader_with_textures;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
pub mod resize;
pub mod resources;
pub mod shader_element;
pub mod shaders;
pub mod snapshot;
pub mod solid_color;
pub mod surface;
pub mod texture;
/// What we're rendering for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -44,39 +51,86 @@ 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>>,
}
/// Render elements split into normal and popup.
#[derive(Debug)]
pub struct SplitElements<E> {
pub normal: Vec<E>,
pub popups: Vec<E>,
}
pub trait ToRenderElement {
type RenderElement;
fn to_render_element(
&self,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
) -> Self::RenderElement;
}
impl RenderTarget {
pub fn should_block_out(self, block_out_from: Option<BlockOutFrom>) -> bool {
match block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => self == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => self != RenderTarget::Output,
}
}
}
impl<E> Default for SplitElements<E> {
fn default() -> Self {
Self {
normal: Vec::new(),
popups: Vec::new(),
}
}
}
impl<E> IntoIterator for SplitElements<E> {
type Item = E;
type IntoIter = std::iter::Chain<std::vec::IntoIter<E>, std::vec::IntoIter<E>>;
fn into_iter(self) -> Self::IntoIter {
self.popups.into_iter().chain(self.normal)
}
}
impl<E> SplitElements<E> {
pub fn iter(&self) -> std::iter::Chain<std::slice::Iter<E>, std::slice::Iter<E>> {
self.popups.iter().chain(&self.normal)
}
pub fn into_vec(self) -> Vec<E> {
let Self { normal, mut popups } = self;
popups.extend(normal);
popups
}
}
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
type RenderElement = PrimaryGpuTextureRenderElement;
fn to_render_element(
&self,
location: Point<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)
@@ -88,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)
}
}
@@ -188,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)
}
@@ -205,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");
@@ -257,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 {
@@ -267,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")?;
}
}
+47 -12
View File
@@ -1,15 +1,15 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use super::texture::{TextureBuffer, TextureRenderElement};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders elements into an off-screen buffer.
@@ -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,
@@ -138,7 +143,7 @@ impl Element for OffscreenRenderElement {
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if let Some(texture) = &self.texture {
texture.opaque_regions(scale)
} else {
@@ -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(())
}
@@ -1,97 +0,0 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
impl Element for PrimaryGpuPixelShaderRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+8 -6
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};
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 {
@@ -40,7 +40,7 @@ impl Element for PrimaryGpuTextureRenderElement {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.0.opaque_regions(scale)
}
@@ -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(())
}
+5 -3
View File
@@ -75,7 +75,7 @@ macro_rules! niri_render_elements {
}
}
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> smithay::backend::renderer::utils::OpaqueRegions<i32, smithay::utils::Physical> {
match self {
$($name::$variant(elem) => elem.opaque_regions(scale)),+
}
@@ -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)
})+
}
}

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