Compare commits

..

214 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
90 changed files with 10059 additions and 3089 deletions
+31 -6
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
@@ -110,7 +110,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@1.77.0
@@ -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:
@@ -172,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
@@ -192,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:
@@ -207,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
+734 -476
View File
File diff suppressed because it is too large Load Diff
+37 -29
View File
@@ -2,22 +2,24 @@
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.8"
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.86"
anyhow = "1.0.93"
bitflags = "2.6.0"
clap = { version = "4.5.14", features = ["derive"] }
serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
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.1", default-features = false }
tracy-client = { version = "0.17.4", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -36,49 +38,53 @@ authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow.workspace = true
arrayvec = "0.7.4"
arrayvec = "0.7.6"
async-channel = "2.3.1"
async-io = { version = "1.13.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.16.3", features = ["derive"] }
calloop = { version = "0.14.0", features = ["executor", "futures-io"] }
bytemuck = { version = "1.19.0", features = ["derive"] }
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.8.0"
fastrand = "2.1.0"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.0"
fastrand = "2.2.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.28.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.155"
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.8", path = "niri-config" }
niri-ipc = { version = "0.1.8", path = "niri-ipc", features = ["clap"] }
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.2.2"
pango = { version = "0.20.0", features = ["v1_44"] }
pangocairo = "0.20.0"
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.13"
portable-atomic = { version = "1.7.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.2"
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.2", optional = true }
xcursor = "0.3.6"
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]
@@ -101,9 +107,9 @@ features = [
[dev-dependencies]
approx = "0.5.1"
k9 = "0.12.0"
k9.workspace = true
proptest = "1.5.0"
proptest-derive = "0.5.0"
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
xshell = "0.2.6"
[features]
@@ -118,6 +124,8 @@ xdp-gnome-screencast = ["dbus", "pipewire"]
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 = []
@@ -131,7 +139,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.8"
version = "0.1.10.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
-2
View File
@@ -49,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.
Generated
+18 -92
View File
@@ -1,65 +1,5 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1720226507,
"narHash": "sha256-yHVvNsgrpyNTXZBEokL8uyB2J6gB1wEx0KOJzoeZi1A=",
"owner": "ipetkov",
"repo": "crane",
"rev": "0aed560c5c0a61c9385bddff471a13036203e11c",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1719815435,
"narHash": "sha256-K2xFp142onP35jcx7li10xUxNVEVRWjAdY8DSuR7Naw=",
"owner": "nix-community",
"repo": "fenix",
"rev": "ebfe2c639111d7e82972a12711206afaeeda2450",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
@@ -77,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1720368505,
"narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=",
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ab82a9612aa45284d4adf69ee81871a389669a9e",
"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": 1719760370,
"narHash": "sha256-fsxAuW6RxKZYjAP3biUC6C4vaYFhDfWv8lp1Tmx3ZCY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "ea7fdada6a0940b239ddbde2048a4d7dac1efe1e",
"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"
}
}
+223 -84
View File
@@ -1,112 +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
xorg.libXcursor
xorg.libXi
libxkbcommon
];
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;} rec {
inherit (niri) LIBCLANG_PATH;
packages = niri.runtimeDependencies ++ niri.nativeBuildInputs ++ niri.buildInputs;
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`
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath packages;
PKG_CONFIG_PATH = pkgs.lib.makeLibraryPath packages;
};
}
);
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 { };
};
};
}
+4 -3
View File
@@ -12,12 +12,13 @@ bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.8", path = "../niri-ipc" }
regex = "1.10.6"
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]
pretty_assertions = "1.4.0"
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
+609 -97
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,12 +1,16 @@
[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 }
+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"
```
+384 -103
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,8 +40,8 @@ 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)]
@@ -17,6 +51,14 @@ pub enum Request {
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.
@@ -32,10 +74,21 @@ pub enum Request {
/// Configuration to apply.
action: OutputAction,
},
/// Request information about workspaces.
Workspaces,
/// Request information about the focused output.
FocusedOutput,
/// 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,
}
@@ -60,16 +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),
/// Information about workspaces.
Workspaces(Vec<Workspace>),
/// Information about the focused output.
FocusedOutput(Option<Output>),
}
/// Actions that niri can perform.
@@ -88,7 +145,9 @@ pub enum Action {
skip_confirmation: bool,
},
/// Power off all monitors via DPMS.
PowerOffMonitors,
PowerOffMonitors {},
/// Power on all monitors via DPMS.
PowerOnMonitors {},
/// Spawn a command.
Spawn {
/// Command to spawn.
@@ -102,85 +161,135 @@ pub enum Action {
delay_ms: Option<u16>,
},
/// Open the screenshot UI.
Screenshot,
Screenshot {},
/// Screenshot the focused screen.
ScreenshotScreen,
/// Screenshot the focused window.
ScreenshotWindow,
/// Close the focused window.
CloseWindow,
/// Toggle fullscreen on the focused window.
FullscreenWindow,
ScreenshotScreen {},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
ScreenshotWindow {
/// Id of the window to screenshot.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Close a window.
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
CloseWindow {
/// Id of the window to close.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle fullscreen on a window.
#[cfg_attr(
feature = "clap",
clap(about = "Toggle fullscreen on the focused window")
)]
FullscreenWindow {
/// Id of the window to toggle fullscreen of.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Focus a window by id.
FocusWindow {
/// Id of the window to focus.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Focus the column to the left.
FocusColumnLeft,
FocusColumnLeft {},
/// Focus the column to the right.
FocusColumnRight,
FocusColumnRight {},
/// Focus the first column.
FocusColumnFirst,
FocusColumnFirst {},
/// Focus the last column.
FocusColumnLast,
FocusColumnLast {},
/// Focus the next column to the right, looping if at end.
FocusColumnRightOrFirst,
FocusColumnRightOrFirst {},
/// Focus the next column to the left, looping if at start.
FocusColumnLeftOrLast,
FocusColumnLeftOrLast {},
/// Focus the window or the monitor above.
FocusWindowOrMonitorUp,
FocusWindowOrMonitorUp {},
/// Focus the window or the monitor below.
FocusWindowOrMonitorDown,
FocusWindowOrMonitorDown {},
/// Focus the column or the monitor to the left.
FocusColumnOrMonitorLeft,
FocusColumnOrMonitorLeft {},
/// Focus the column or the monitor to the right.
FocusColumnOrMonitorRight,
FocusColumnOrMonitorRight {},
/// Focus the window below.
FocusWindowDown,
FocusWindowDown {},
/// Focus the window above.
FocusWindowUp,
FocusWindowUp {},
/// Focus the window below or the column to the left.
FocusWindowDownOrColumnLeft,
FocusWindowDownOrColumnLeft {},
/// Focus the window below or the column to the right.
FocusWindowDownOrColumnRight,
FocusWindowDownOrColumnRight {},
/// Focus the window above or the column to the left.
FocusWindowUpOrColumnLeft,
FocusWindowUpOrColumnLeft {},
/// Focus the window above or the column to the right.
FocusWindowUpOrColumnRight,
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,
MoveColumnLeftOrToMonitorLeft {},
/// Move the focused column to the right or to the monitor to the right.
MoveColumnRightOrToMonitorRight,
MoveColumnRightOrToMonitorRight {},
/// Move the focused window down in a column.
MoveWindowDown,
MoveWindowDown {},
/// Move the focused window up in a column.
MoveWindowUp,
MoveWindowUp {},
/// Move the focused window down in a column or to the workspace below.
MoveWindowDownOrToWorkspaceDown,
MoveWindowDownOrToWorkspaceDown {},
/// Move the focused window up in a column or to the workspace above.
MoveWindowUpOrToWorkspaceUp,
/// Consume or expel the focused window left.
ConsumeOrExpelWindowLeft,
/// Consume or expel the focused window right.
ConsumeOrExpelWindowRight,
MoveWindowUpOrToWorkspaceUp {},
/// Consume or expel a window left.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window left")
)]
ConsumeOrExpelWindowLeft {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume or expel a window right.
#[cfg_attr(
feature = "clap",
clap(about = "Consume or expel the focused window right")
)]
ConsumeOrExpelWindowRight {
/// Id of the window to consume or expel.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn,
ConsumeWindowIntoColumn {},
/// Expel the focused window from the column.
ExpelWindowFromColumn,
ExpelWindowFromColumn {},
/// Center the focused column on the screen.
CenterColumn,
CenterColumn {},
/// Focus the workspace below.
FocusWorkspaceDown,
FocusWorkspaceDown {},
/// Focus the workspace above.
FocusWorkspaceUp,
FocusWorkspaceUp {},
/// Focus a workspace by reference (index or name).
FocusWorkspace {
/// Reference (index or name) of the workspace to focus.
@@ -188,21 +297,31 @@ pub enum Action {
reference: WorkspaceReferenceArg,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
FocusWorkspacePrevious {},
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceDown {},
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by reference (index or name).
MoveWindowToWorkspaceUp {},
/// Move a window to a workspace.
#[cfg_attr(
feature = "clap",
clap(about = "Move the focused window to a workspace by reference (index or name)")
)]
MoveWindowToWorkspace {
/// Id of the window to move.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
window_id: Option<u64>,
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceDown {},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
MoveColumnToWorkspaceUp {},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
@@ -210,45 +329,73 @@ pub enum Action {
reference: WorkspaceReferenceArg,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
MoveWorkspaceDown {},
/// Move the focused workspace up.
MoveWorkspaceUp,
MoveWorkspaceUp {},
/// Focus the monitor to the left.
FocusMonitorLeft,
FocusMonitorLeft {},
/// Focus the monitor to the right.
FocusMonitorRight,
FocusMonitorRight {},
/// Focus the monitor below.
FocusMonitorDown,
FocusMonitorDown {},
/// Focus the monitor above.
FocusMonitorUp,
FocusMonitorUp {},
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft,
MoveWindowToMonitorLeft {},
/// Move the focused window to the monitor to the right.
MoveWindowToMonitorRight,
MoveWindowToMonitorRight {},
/// Move the focused window to the monitor below.
MoveWindowToMonitorDown,
MoveWindowToMonitorDown {},
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp,
MoveWindowToMonitorUp {},
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft,
MoveColumnToMonitorLeft {},
/// Move the focused column to the monitor to the right.
MoveColumnToMonitorRight,
MoveColumnToMonitorRight {},
/// Move the focused column to the monitor below.
MoveColumnToMonitorDown,
MoveColumnToMonitorDown {},
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp,
/// Change the height of the focused window.
MoveColumnToMonitorUp {},
/// Change the height of a window.
#[cfg_attr(
feature = "clap",
clap(about = "Change the height of the focused window")
)]
SetWindowHeight {
/// Id of the window whose height to set.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Reset the height of the focused window back to automatic.
ResetWindowHeight,
/// Reset the height of a window back to automatic.
#[cfg_attr(
feature = "clap",
clap(about = "Reset the height of the focused window back to automatic")
)]
ResetWindowHeight {
/// Id of the window whose height to reset.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset column widths.
SwitchPresetColumnWidth,
SwitchPresetColumnWidth {},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle the maximized state of the focused column.
MaximizeColumn,
MaximizeColumn {},
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
@@ -262,21 +409,21 @@ pub enum Action {
layout: LayoutSwitchTarget,
},
/// Show the hotkey overlay.
ShowHotkeyOverlay,
ShowHotkeyOverlay {},
/// Move the focused workspace to the monitor to the left.
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorLeft {},
/// Move the focused workspace to the monitor to the right.
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorRight {},
/// Move the focused workspace to the monitor below.
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorDown {},
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp,
MoveWorkspaceToMonitorUp {},
/// Toggle a debug tint on windows.
ToggleDebugTint,
ToggleDebugTint {},
/// Toggle visualization of render element opaque regions.
DebugToggleOpaqueRegions,
DebugToggleOpaqueRegions {},
/// Toggle visualization of output damage.
DebugToggleDamage,
DebugToggleDamage {},
}
/// Change in window or column size.
@@ -293,10 +440,12 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Workspace reference (index or name) to operate on.
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum WorkspaceReferenceArg {
/// Id of the workspace.
Id(u64),
/// Index of the workspace.
Index(u8),
/// Name of the workspace.
@@ -352,18 +501,11 @@ pub enum OutputAction {
#[cfg_attr(feature = "clap", command(subcommand))]
position: PositionToSet,
},
/// Toggle variable refresh rate.
/// Set the variable refresh rate mode.
Vrr {
/// Whether to enable variable refresh rate.
#[cfg_attr(
feature = "clap",
arg(
value_name = "ON|OFF",
action = clap::ArgAction::Set,
value_parser = clap::builder::BoolishValueParser::new(),
),
)]
enable: bool,
/// Variable refresh rate mode to set.
#[cfg_attr(feature = "clap", command(flatten))]
vrr: VrrToSet,
},
}
@@ -425,6 +567,27 @@ pub struct ConfiguredPosition {
pub y: i32,
}
/// Output VRR to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct VrrToSet {
/// Whether to enable variable refresh rate.
#[cfg_attr(
feature = "clap",
arg(
value_name = "ON|OFF",
action = clap::ArgAction::Set,
value_parser = clap::builder::BoolishValueParser::new(),
hide_possible_values = true,
),
)]
pub vrr: bool,
/// Only enable when the output shows a window matching the variable-refresh-rate window rule.
#[cfg_attr(feature = "clap", arg(long))]
pub on_demand: bool,
}
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -435,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.
@@ -518,10 +683,24 @@ pub enum Transform {
#[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.
@@ -538,9 +717,22 @@ pub enum OutputConfigChanged {
#[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>,
@@ -549,7 +741,96 @@ pub struct Workspace {
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
pub is_active: bool,
/// Whether the workspace is currently focused.
///
/// There's only one focused workspace across all outputs.
pub is_focused: bool,
/// Id of the active window on this workspace, if any.
pub active_window_id: Option<u64>,
}
/// Configured keyboard layouts.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct KeyboardLayouts {
/// XKB names of the configured layouts.
pub names: Vec<String>,
/// Index of the currently active layout in `names`.
pub current_idx: u8,
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum Event {
/// The workspace configuration has changed.
WorkspacesChanged {
/// The new workspace configuration.
///
/// This configuration completely replaces the previous configuration. I.e. if any
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
/// workspace on its output. All other workspaces on the same output become inactive.
WorkspaceActivated {
/// Id of the newly active workspace.
id: u64,
/// Whether this workspace also became focused.
///
/// If `true`, this is now the single focused workspace. All other workspaces are no longer
/// focused, but they may remain active on their respective outputs.
focused: bool,
},
/// An active window changed on a workspace.
WorkspaceActiveWindowChanged {
/// Id of the workspace on which the active window changed.
workspace_id: u64,
/// Id of the new active window, if any.
active_window_id: Option<u64>,
},
/// The window configuration has changed.
WindowsChanged {
/// The new window configuration.
///
/// This configuration completely replaces the previous configuration. I.e. if any windows
/// are missing from here, then they were closed.
windows: Vec<Window>,
},
/// A new toplevel window was opened, or an existing toplevel window changed.
WindowOpenedOrChanged {
/// The new or updated window.
///
/// If the window is focused, all other windows are no longer focused.
window: Window,
},
/// A toplevel window was closed.
WindowClosed {
/// Id of the removed window.
id: u64,
},
/// Window focus changed.
///
/// All other windows are no longer focused.
WindowFocusChanged {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
keyboard_layouts: KeyboardLayouts,
},
/// The keyboard layout switched.
KeyboardLayoutSwitched {
/// Index of the newly active layout.
idx: u8,
},
}
impl FromStr for WorkspaceReferenceArg {
@@ -560,7 +841,7 @@ impl FromStr for WorkspaceReferenceArg {
if let Ok(idx) = u8::try_from(index) {
Self::Index(idx)
} else {
return Err("workspace indexes must be between 0 and 255");
return Err("workspace index must be between 0 and 255");
}
} else {
Self::Name(s.to_string())
+23 -9
View File
@@ -1,12 +1,12 @@
//! Helper for blocking communication over the niri socket.
use std::env;
use std::io::{self, Read, Write};
use std::io::{self, BufRead, BufReader, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::{Reply, Request};
use crate::{Event, Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
@@ -47,17 +47,31 @@ impl Socket {
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
pub fn send(self, request: Request) -> io::Result<Reply> {
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_vec(&request).unwrap();
stream.write_all(&buf)?;
let mut buf = serde_json::to_string(&request).unwrap();
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
buf.clear();
stream.read_to_end(&mut buf)?;
let mut reader = BufReader::new(stream);
let reply = serde_json::from_slice(&buf)?;
Ok(reply)
buf.clear();
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
let events = move || {
buf.clear();
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}
+194
View File
@@ -0,0 +1,194 @@
//! Helpers for keeping track of the event stream state.
//!
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
//! you only care about part of the state.
//! 2. Connect to the niri socket and request an event stream.
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
//! 4. Read the fields of the state as needed.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use crate::{Event, KeyboardLayouts, Window, Workspace};
/// Part of the state communicated via the event stream.
pub trait EventStreamStatePart {
/// Returns a sequence of events that replicates this state from default initialization.
fn replicate(&self) -> Vec<Event>;
/// Applies the event to this state.
///
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
/// part of the state.
fn apply(&mut self, event: Event) -> Option<Event>;
}
/// The full state communicated over the event stream.
///
/// Different parts of the state are not guaranteed to be consistent across every single event
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
/// these two events, the workspace active window id refers to a window that does not yet exist in
/// the windows state part.
#[derive(Debug, Default)]
pub struct EventStreamState {
/// State of workspaces.
pub workspaces: WorkspacesState,
/// State of workspaces.
pub windows: WindowsState,
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
}
/// The workspaces state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WorkspacesState {
/// Map from a workspace id to the workspace.
pub workspaces: HashMap<u64, Workspace>,
}
/// The windows state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WindowsState {
/// Map from a window id to the window.
pub windows: HashMap<u64, Window>,
}
/// The keyboard layout state communicated over the event stream.
#[derive(Debug, Default)]
pub struct KeyboardLayoutsState {
/// Configured keyboard layouts.
pub keyboard_layouts: Option<KeyboardLayouts>,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events
}
fn apply(&mut self, event: Event) -> Option<Event> {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
Some(event)
}
}
impl EventStreamStatePart for WorkspacesState {
fn replicate(&self) -> Vec<Event> {
let workspaces = self.workspaces.values().cloned().collect();
vec![Event::WorkspacesChanged { workspaces }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
let output = ws.output.clone();
for ws in self.workspaces.values_mut() {
let got_activated = ws.id == id;
if ws.output == output {
ws.is_active = got_activated;
}
if focused {
ws.is_focused = got_activated;
}
}
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
let ws = self.workspaces.get_mut(&workspace_id);
let ws = ws.expect("changed workspace was missing from the map");
ws.active_window_id = active_window_id;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for WindowsState {
fn replicate(&self) -> Vec<Event> {
let windows = self.windows.values().cloned().collect();
vec![Event::WindowsChanged { windows }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WindowsChanged { windows } => {
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
}
Event::WindowOpenedOrChanged { window } => {
let (id, is_focused) = match self.windows.entry(window.id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
*entry = window;
(entry.id, entry.is_focused)
}
Entry::Vacant(entry) => {
let entry = entry.insert(window);
(entry.id, entry.is_focused)
}
};
if is_focused {
for win in self.windows.values_mut() {
if win.id != id {
win.is_focused = false;
}
}
}
}
Event::WindowClosed { id } => {
let win = self.windows.remove(&id);
win.expect("closed window was missing from the map");
}
Event::WindowFocusChanged { id } => {
for win in self.windows.values_mut() {
win.is_focused = Some(win.id) == id;
}
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for KeyboardLayoutsState {
fn replicate(&self) -> Vec<Event> {
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
} else {
vec![]
}
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
self.keyboard_layouts = Some(keyboard_layouts);
}
Event::KeyboardLayoutSwitched { idx } => {
let kb = self.keyboard_layouts.as_mut();
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
kb.current_idx = idx;
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.0", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.8", path = ".." }
niri-config = { version = "0.1.8", path = "../niri-config" }
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
+11 -10
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, FloatOrInt};
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 {
@@ -147,7 +153,7 @@ impl Layout {
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout.add_window(window.clone(), width, false);
@@ -161,7 +167,7 @@ impl Layout {
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout
@@ -192,11 +198,7 @@ impl TestCase for Layout {
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
@@ -222,12 +224,11 @@ impl TestCase for Layout {
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout.update_render_elements(&self.output);
self.layout.update_render_elements(Some(&self.output));
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
+4 -4
View File
@@ -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.to_f64(), 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.to_f64(), 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.to_f64(), false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -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)).to_f64(), false);
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
self.window.communicate();
}
+5 -4
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();
}
+2 -2
View File
@@ -16,7 +16,7 @@ mod imp {
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::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() {
+18 -3
View File
@@ -3,11 +3,13 @@ use std::cmp::{max, min};
use std::rc::Rc;
use niri::layout::{
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::{self, Output};
@@ -85,7 +87,7 @@ impl TestWindow {
let mut new_size = inner.size;
if let Some(size) = inner.requested_size.take() {
if let Some(size) = inner.requested_size {
assert!(size.w >= 0);
assert!(size.h >= 0);
@@ -176,7 +178,12 @@ impl LayoutElement for TestWindow {
}
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
@@ -215,6 +222,10 @@ impl LayoutElement for TestWindow {
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
@@ -225,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 {
+1
View File
@@ -68,6 +68,7 @@ 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
+30 -4
View File
@@ -40,6 +40,16 @@ input {
// 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
@@ -113,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.
@@ -199,7 +212,9 @@ layout {
// 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.
@@ -247,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
@@ -419,15 +441,18 @@ binds {
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// Consume one window from the right into the focused column.
Mod+Comma { consume-window-into-column; }
// Expel one window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// There are also commands that consume or expel a single window to the side.
// Mod+BracketLeft { consume-or-expel-window-left; }
// Mod+BracketRight { consume-or-expel-window-right; }
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { reset-window-height; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
@@ -461,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 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
+6
View File
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
</head>
</html>
+4 -4
View File
@@ -11,7 +11,7 @@ pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Animation {
from: f64,
to: f64,
@@ -101,9 +101,9 @@ impl Animation {
}
/// Restarts the animation using the previous config.
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
if self.is_off {
return self;
return self.clone();
}
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
@@ -292,7 +292,7 @@ impl Animation {
return self.to;
}
let passed = self.current_time - self.start_time;
let passed = self.current_time.saturating_sub(self.start_time);
match self.kind {
Kind::Easing { curve } => {
+17 -2
View File
@@ -37,14 +37,14 @@ pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutputId(u32);
pub struct OutputId(u64);
impl OutputId {
fn next() -> OutputId {
OutputId(OUTPUT_ID_COUNTER.next())
}
pub fn get(self) -> u32 {
pub fn get(self) -> u64 {
self.0
}
}
@@ -153,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),
@@ -167,6 +174,14 @@ impl Backend {
}
}
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
if let Self::Tty(v) = self {
Some(v)
} else {
None
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v
+322 -206
View File
@@ -4,7 +4,6 @@ use std::fmt::Write;
use std::iter::zip;
use std::num::NonZeroU64;
use std::os::fd::AsFd;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -14,7 +13,7 @@ use std::{io, mem};
use anyhow::{anyhow, bail, ensure, Context};
use bytemuck::cast_slice_mut;
use libc::dev_t;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
@@ -52,7 +51,6 @@ use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
@@ -63,7 +61,7 @@ use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -178,8 +176,9 @@ struct TtyOutputState {
}
struct Surface {
name: String,
name: OutputName,
compositor: GbmDrmCompositor,
connector: connector::Handle,
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
@@ -426,18 +425,6 @@ impl Tty {
}
// Restore VRR.
let Some(connector) =
surface.compositor.pending_connectors().into_iter().next()
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector)
else {
error!("missing enabled connector in drm_scanner");
continue;
};
let output = niri
.global_space
.outputs()
@@ -451,13 +438,13 @@ impl Tty {
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
error!("missing state for output {:?}", surface.name.connector);
continue;
};
try_to_change_vrr(
&device.drm,
connector,
surface.connector,
*crtc,
surface,
output_state,
@@ -649,22 +636,31 @@ impl Tty {
connector,
crtc: Some(crtc),
} => {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
let connector_name = format_connector_name(&connector);
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name, false);
debug!(
"new connector: {} \"{}\"",
&output_name.connector,
output_name.format_make_model_serial(),
);
// Assign an id to this crtc.
device.output_ids.insert(crtc, OutputId::next());
}
DrmScanEvent::Disconnected {
crtc: Some(crtc), ..
} => {
self.connector_disconnected(niri, node, crtc);
removed.push(crtc);
}
_ => (),
}
}
// FIXME: this is better done in connector_disconnected(), but currently we call that to
// turn off outputs temporarily, too. So we can't do this there.
for crtc in &removed {
self.connector_disconnected(niri, node, *crtc);
}
let Some(device) = self.devices.get_mut(&node) else {
error!("device disappeared");
return;
@@ -676,7 +672,12 @@ impl Tty {
}
}
self.refresh_ipc_outputs(niri);
// This will connect any new connectors if needed, and apply other changes, such as
// connecting back the internal laptop monitor once it becomes the only monitor left.
//
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
// above.
self.on_output_config_changed(niri);
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -757,26 +758,27 @@ impl Tty {
connector: connector::Info,
crtc: crtc::Handle,
) -> anyhow::Result<()> {
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
debug!("connecting connector: {output_name}");
let connector_name = format_connector_name(&connector);
debug!("connecting connector: {connector_name}");
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name.clone(),
self.config.borrow().debug.disable_monitor_names,
);
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
.unwrap_or(false);
if non_desktop {
debug!("output is non desktop");
let description = get_edid_info(&device.drm, connector.handle())
.map(|info| truncate_to_nul(info.model))
.unwrap_or_else(|| "Unknown".into());
let description = output_name.format_description();
if let Some(lease_state) = &mut device.drm_lease_state {
lease_state.add_connector::<State>(connector.handle(), output_name, description);
lease_state.add_connector::<State>(connector.handle(), connector_name, description);
}
device
.non_desktop_connectors
@@ -784,10 +786,6 @@ impl Tty {
return Ok(());
}
// This should be unique per CRTC connection, however currently we can call
// connector_connected() multiple times for turning the output off and on.
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
let config = self
.config
.borrow()
@@ -796,11 +794,6 @@ impl Tty {
.cloned()
.unwrap_or_default();
if config.off {
debug!("output is disabled in the config");
return Ok(());
}
for m in connector.modes() {
trace!("{m:?}");
}
@@ -832,15 +825,13 @@ impl Tty {
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
let word = if config.variable_refresh_rate {
"enabling"
} else {
"disabling"
};
// Even if on-demand, we still disable it until later checks.
let vrr = config.is_vrr_always_on();
let word = if vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
if enabled != config.variable_refresh_rate {
if enabled != vrr {
warn!("failed {} VRR", word);
}
@@ -851,13 +842,13 @@ impl Tty {
}
}
} else {
if config.variable_refresh_rate {
if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate);
let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
@@ -865,7 +856,7 @@ impl Tty {
vrr_enabled = true;
}
}
} else if config.variable_refresh_rate {
} else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
@@ -894,22 +885,13 @@ impl Tty {
// Update the output mode.
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output = Output::new(
output_name.clone(),
connector_name.clone(),
PhysicalProperties {
size: (physical_width as i32, physical_height as i32).into(),
subpixel: connector.subpixel().into(),
model,
make,
model: output_name.model.as_deref().unwrap_or("Unknown").to_owned(),
make: output_name.make.as_deref().unwrap_or("Unknown").to_owned(),
},
);
@@ -920,6 +902,7 @@ impl Tty {
output
.user_data()
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
@@ -944,6 +927,11 @@ impl Tty {
// Filter out the CCS modifiers as they have increased bandwidth, causing some monitor
// configurations to stop working.
//
// The invalid modifier attempt below should make this unnecessary in some cases, but it
// would still be a bad idea to remove this until Smithay has some kind of full-device
// modesetting test that is able to "downgrade" existing connector modifiers to get enough
// bandwidth for a newly connected one.
let render_formats = render_formats
.iter()
.copied()
@@ -968,19 +956,55 @@ impl Tty {
.collect::<FormatSet>();
// Create the compositor.
let mut compositor = DrmCompositor::new(
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
allocator,
allocator.clone(),
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
// This is only used to pick a good internal format, so it can use the surface's render
// formats, even though we only ever render on the primary GPU.
render_formats.clone(),
device.drm.cursor_size(),
cursor_plane_gbm,
)?;
cursor_plane_gbm.clone(),
);
let mut compositor = match res {
Ok(x) => x,
Err(err) => {
warn!("error creating DRM compositor, will try with invalid modifier: {err:?}");
let render_formats = render_formats
.iter()
.copied()
.filter(|format| format.modifier == Modifier::Invalid)
.collect::<FormatSet>();
// DrmCompositor::new() consumed the surface...
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
let mut planes = surface.planes().clone();
if !config.debug.enable_overlay_planes {
planes.overlay.clear();
}
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
surface,
Some(planes),
allocator,
device.gbm.clone(),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
cursor_plane_gbm,
)
.context("error creating DRM compositor")?
}
};
if self.debug_tint {
compositor.set_debug_flags(DebugFlags::TINT);
}
@@ -1005,18 +1029,28 @@ impl Tty {
}
}
// Some buggy monitors replug upon powering off, so powering on here would prevent such
// monitors from powering off. Therefore, we avoid unconditionally powering on.
if !niri.monitors_active {
if let Err(err) = compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {output_name}"));
let time_since_presentation_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} time since presentation, ms"));
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
"{connector_name} time since presentation, ms"
));
let presentation_misprediction_plot_name = tracy_client::PlotName::new_leak(format!(
"{output_name} presentation misprediction, ms"
"{connector_name} presentation misprediction, ms"
));
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
tracy_client::PlotName::new_leak(format!("{connector_name} sequence delta"));
let surface = Surface {
name: output_name.clone(),
name: output_name,
connector: connector.handle(),
compositor,
dmabuf_feedback,
gamma_props,
@@ -1034,15 +1068,14 @@ impl Tty {
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
// Some buggy monitors replug upon powering off, so powering on here would prevent such
// monitors from powering off. Therefore, we avoid unconditionally powering on.
if niri.monitors_active {
// Redraw the new monitor.
niri.event_loop.insert_idle(move |state| {
state.niri.queue_redraw(&output);
// Guard against output disconnecting before the idle has a chance to run.
if state.niri.output_state.contains_key(&output) {
state.niri.queue_redraw(&output);
}
});
} else {
set_crtc_active(&device.drm, crtc, false);
}
Ok(())
@@ -1078,7 +1111,7 @@ impl Tty {
return;
};
debug!("disconnecting connector: {:?}", surface.name);
debug!("disconnecting connector: {:?}", surface.name.connector);
let output = niri
.global_space
@@ -1120,7 +1153,7 @@ impl Tty {
// Finish the Tracy frame, if any.
drop(surface.vblank_frame.take());
let name = &surface.name;
let name = &surface.name.connector;
trace!("vblank on {name} {meta:?}");
span.emit_text(name);
@@ -1178,15 +1211,24 @@ impl Tty {
return;
};
// This happened for someone reconnecting 2 monitors with a KVM switch:
// https://github.com/YaLTeR/niri/issues/556
//
// Maybe the vblank didn't get cancelled or got reordered weirdly? Either way, we can avoid
// crashing here.
if matches!(output_state.redraw_state, RedrawState::Idle) {
error!("got vblank for an idle output {name}");
return;
}
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
state @ (RedrawState::Idle
| RedrawState::Queued
| RedrawState::WaitingForEstimatedVBlank(_)
| RedrawState::WaitingForEstimatedVBlankAndQueued(_)) => {
// This is an error!() because it shouldn't happen, but on some systems it somehow
// does. Kernel sending rogue vblank events?
//
// https://github.com/YaLTeR/niri/issues/556
// https://github.com/YaLTeR/niri/issues/615
error!(
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
can happen when resuming from sleep or powering on monitors: {state:?}"
);
true
}
};
// Mark the last frame as submitted.
match surface.compositor.frame_submitted() {
@@ -1234,14 +1276,6 @@ impl Tty {
output_state.frame_clock.presented(presentation_time);
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued => unreachable!(),
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
};
if redraw_needed || output_state.unfinished_animations_remain {
let vblank_frame = tracy_client::Client::running()
.unwrap()
@@ -1323,7 +1357,7 @@ impl Tty {
return rv;
};
span.emit_text(&surface.name);
span.emit_text(&surface.name.connector);
if !device.drm.is_active() {
warn!("device is inactive");
@@ -1356,12 +1390,13 @@ impl Tty {
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
.borrow()
.debug
.wait_for_frame_completion_before_queueing
{
let needs_sync = res.needs_sync()
|| self
.config
.borrow()
.debug
.wait_for_frame_completion_before_queueing;
if needs_sync {
if let PrimaryPlaneElement::Swapchain(element) = res.primary_element {
let _span = tracy_client::span!("wait for completion");
if let Err(err) = element.sync.wait() {
@@ -1537,22 +1572,14 @@ impl Tty {
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name.clone(),
self.config.borrow().debug.disable_monitor_names,
);
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1600,9 +1627,10 @@ impl Tty {
.map(logical_output);
let ipc_output = niri_ipc::Output {
name,
make,
model,
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
model: output_name.model.unwrap_or_else(|| "Unknown".into()),
serial: output_name.serial,
physical_size,
modes,
current_mode,
@@ -1652,10 +1680,36 @@ impl Tty {
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
for surface in device.surfaces.values_mut() {
if let Err(err) = surface.compositor.clear() {
warn!("error clearing drm surface: {err:?}");
}
}
}
}
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
let _span = tracy_client::span!("Tty::set_output_on_demand_vrr");
let output_state = niri.output_state.get_mut(output).unwrap();
output_state.on_demand_vrr_enabled = enable_vrr;
if output_state.frame_clock.vrr() == enable_vrr {
return;
}
for (&node, device) in self.devices.iter_mut() {
for (&crtc, surface) in device.surfaces.iter_mut() {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
if tty_state.node == node && tty_state.crtc == crtc {
try_to_change_vrr(
&device.drm,
surface.connector,
crtc,
surface,
output_state,
enable_vrr,
);
self.refresh_ipc_outputs(niri);
return;
}
}
}
@@ -1671,13 +1725,29 @@ impl Tty {
}
self.update_output_config_on_resume = false;
// Figure out if we should disable laptop panels.
let mut disable_laptop_panels = false;
if niri.is_lid_closed {
let config = self.config.borrow();
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
// Check if any external monitor is connected.
'outer: for device in self.devices.values() {
for (connector, _crtc) in device.drm_scanner.crtcs() {
if !is_laptop_panel(&format_connector_name(connector)) {
disable_laptop_panels = true;
break 'outer;
}
}
}
}
}
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
let mut to_disconnect = vec![];
let mut to_connect = vec![];
for (&node, device) in &mut self.devices {
for surface in device.surfaces.values_mut() {
let crtc = surface.compositor.crtc();
for (&crtc, surface) in device.surfaces.iter_mut() {
let config = self
.config
.borrow()
@@ -1685,18 +1755,14 @@ impl Tty {
.find(&surface.name)
.cloned()
.unwrap_or_default();
if config.off {
if config.off || should_disable(&surface.name.connector) {
to_disconnect.push((node, crtc));
continue;
}
// Check if we need to change the mode.
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
let Some(connector) = device.drm_scanner.connectors().get(&surface.connector)
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
};
@@ -1707,8 +1773,9 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
let change_vrr = surface.vrr_enabled != config.variable_refresh_rate;
if !change_mode && !change_vrr {
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
let is_on_demand_vrr = config.is_vrr_on_demand();
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1725,18 +1792,20 @@ impl Tty {
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
error!("missing state for output {:?}", surface.name.connector);
continue;
};
if change_vrr {
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
|| (!is_on_demand_vrr && change_always_vrr)
{
try_to_change_vrr(
&device.drm,
connector,
connector.handle(),
crtc,
surface,
output_state,
config.variable_refresh_rate,
!surface.vrr_enabled,
);
}
@@ -1746,7 +1815,7 @@ impl Tty {
warn!(
"output {:?}: configured mode {}x{}{} could not be found, \
falling back to preferred",
surface.name,
surface.name.connector,
target.width,
target.height,
if let Some(refresh) = target.refresh {
@@ -1757,7 +1826,10 @@ impl Tty {
);
}
debug!("output {:?}: picking mode: {mode:?}", surface.name);
debug!(
"output {:?}: picking mode: {mode:?}",
surface.name.connector
);
if let Err(err) = surface.compositor.use_mode(mode) {
warn!("error changing mode: {err:?}");
continue;
@@ -1779,16 +1851,21 @@ impl Tty {
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
continue;
}
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
let connector_name = format_connector_name(connector);
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name,
self.config.borrow().debug.disable_monitor_names,
);
let config = self
.config
.borrow()
@@ -1797,8 +1874,8 @@ impl Tty {
.cloned()
.unwrap_or_default();
if !config.off {
to_connect.push((node, connector.clone(), crtc));
if !(config.off || should_disable(&output_name.connector)) {
to_connect.push((node, connector.clone(), crtc, output_name));
}
}
}
@@ -1807,7 +1884,11 @@ impl Tty {
self.connector_disconnected(niri, node, crtc);
}
for (node, connector, crtc) in to_connect {
// Sort by output name to get more predictable first focused output at initial compositor
// startup, when multiple connectors appear at once.
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
for (node, connector, crtc, _name) in to_connect {
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
warn!("error connecting connector: {err:?}");
}
@@ -1832,6 +1913,39 @@ impl Tty {
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
continue;
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc)
|| device
.non_desktop_connectors
.contains(&(connector.handle(), crtc))
{
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name,
self.config.borrow().debug.disable_monitor_names,
);
if output_name.matches(target) {
return Some(output_name);
}
}
}
None
}
}
impl GammaProps {
@@ -1937,14 +2051,13 @@ impl GammaProps {
property::Value::Blob(blob).into(),
)
.context("error setting GAMMA_LUT")
.map_err(|err| {
.inspect_err(|_| {
if blob != 0 {
// Destroy the blob we just allocated.
if let Err(err) = device.destroy_property_blob(blob) {
warn!("error destroying GAMMA_LUT property blob: {err:?}");
}
}
err
})?;
}
@@ -2101,17 +2214,6 @@ fn get_drm_property(
.find_map(|(handle, value)| (handle == prop).then_some(value))
}
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
return;
};
let value = property::Value::Boolean(active);
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
warn!("error setting CRTC property: {err:?}");
}
}
fn refresh_interval(mode: DrmMode) -> Duration {
let clock = mode.clock() as u64;
let htotal = mode.hsync().2 as u64;
@@ -2264,23 +2366,21 @@ fn pick_mode(
mode.map(|m| (*m, fallback))
}
fn truncate_to_nul(mut s: String) -> String {
if let Some(index) = s.find('\0') {
s.truncate(index);
}
s
}
fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<EdidInfo> {
match catch_unwind(AssertUnwindSafe(move || {
EdidInfo::for_connector(device, connector)
})) {
Ok(info) => info,
Err(err) => {
warn!("edid-rs panicked: {err:?}");
None
}
}
fn get_edid_info(
device: &DrmDevice,
connector: connector::Handle,
) -> anyhow::Result<libdisplay_info::info::Info> {
let (_, info, value) =
find_drm_property(device, connector, "EDID").context("no EDID property")?;
let blob = info
.value_type()
.convert_value(value)
.as_blob()
.context("EDID was not blob type")?;
let data = device
.get_property_blob(blob)
.context("error getting EDID blob value")?;
libdisplay_info::info::Info::parse_edid(&data).context("error parsing EDID")
}
fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> anyhow::Result<u64> {
@@ -2388,7 +2488,7 @@ pub fn set_gamma_for_crtc(
fn try_to_change_vrr(
device: &DrmDevice,
connector: &connector::Info,
connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
@@ -2396,47 +2496,63 @@ fn try_to_change_vrr(
) {
let _span = tracy_client::span!("try_to_change_vrr");
if is_vrr_capable(device, connector.handle()) == Some(true) {
if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name, word);
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!("output {:?}: error {} VRR: {err:?}", surface.name, word);
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name
surface.name.connector
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
)
}
#[track_caller]
fn check(input: &str, expected: &str) {
let input = String::from(input);
assert_eq!(truncate_to_nul(input), expected);
fn make_output_name(
device: &DrmDevice,
connector: connector::Handle,
connector_name: String,
disable_monitor_names: bool,
) -> OutputName {
if disable_monitor_names {
return OutputName {
connector: connector_name,
make: None,
model: None,
serial: None,
};
}
#[test]
fn truncate_to_nul_works() {
check("", "");
check("qwer", "qwer");
check("abc\0def", "abc");
check("\0as", "");
check("a\0\0\0b", "a");
check("bb😁\0cc", "bb😁");
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
OutputName {
connector: connector_name,
make: info.as_ref().and_then(|info| info.make()),
model: info.as_ref().and_then(|info| info.model()),
serial: info.as_ref().and_then(|info| info.serial()),
}
}
+9 -1
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;
@@ -59,6 +59,13 @@ 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([(
OutputId::next(),
@@ -66,6 +73,7 @@ impl Winit {
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,
+8 -2
View File
@@ -62,10 +62,14 @@ pub enum Msg {
Outputs,
/// List workspaces.
Workspaces,
/// Print information about the focused window.
FocusedWindow,
/// 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.
Action {
#[command(subcommand)]
@@ -86,6 +90,8 @@ pub enum Msg {
#[command(subcommand)]
action: OutputAction,
},
/// Start continuously receiving events from the compositor.
EventStream,
/// Print the version of the running niri instance.
Version,
/// Request an error from the running niri instance.
+63 -21
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>>,
@@ -63,19 +64,14 @@ impl DisplayConfig {
.map(|output| {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
// FIXME: use proper serial when we have libdisplay-info.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let 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),
@@ -111,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,
};
@@ -144,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))]);
@@ -183,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″");
}
}
+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.
+128 -45
View File
@@ -1,15 +1,16 @@
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, with_states,
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
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;
@@ -19,6 +20,7 @@ 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 {
@@ -46,44 +48,12 @@ impl CompositorHandler for State {
}
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
.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);
}
}
}
}
});
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);
@@ -157,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();
@@ -222,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);
});
}
@@ -242,10 +216,17 @@ impl CompositorHandler for State {
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: u64::from(id.get()),
id: id.get(),
});
self.niri.layout.remove_window(&window);
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();
@@ -303,22 +284,68 @@ impl CompositorHandler for State {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(&output.clone());
}
return;
}
// This might be a layer-shell surface.
self.layer_shell_handle_commit(surface);
if self.layer_shell_handle_commit(surface) {
return;
}
// This might be a cursor surface.
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
{
if matches!(
&self.niri.cursor_manager.cursor_image(),
CursorImageStatus::Surface(s) if s == &root_surface
) {
// In case the cursor surface has been committed handle the role specific
// buffer offset by applying the offset on the cursor image hotspot
if surface == &root_surface {
with_states(surface, |states| {
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
if let Some(mut cursor_image_attributes) =
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
{
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take();
if let Some(buffer_delta) = buffer_delta {
cursor_image_attributes.hotspot -= buffer_delta;
}
}
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a DnD icon surface.
if self.niri.dnd_icon.as_ref() == Some(surface) {
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
// In case the dnd surface has been committed handle the role specific
// buffer offset by applying the offset on the dnd icon offset
if surface == &dnd_icon.surface {
with_states(&dnd_icon.surface, |states| {
let buffer_delta = states
.cached_state
.get::<SurfaceAttributes>()
.current()
.buffer_delta
.take()
.unwrap_or_default();
dnd_icon.offset += buffer_delta;
});
}
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
return;
}
// This might be a lock surface.
@@ -327,7 +354,7 @@ impl CompositorHandler for State {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == &root_surface {
self.niri.queue_redraw(&output.clone());
break;
return;
}
}
}
@@ -356,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);
}
}
@@ -371,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");
}
}
}
+88 -31
View File
@@ -1,11 +1,12 @@
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::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;
@@ -36,12 +37,19 @@ impl WlrLayerShellHandler for State {
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);
@@ -68,52 +76,101 @@ impl WlrLayerShellHandler for State {
delegate_layer_shell!(State);
impl State {
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
let Some(output) = self
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
let mut root_surface = surface.clone();
while let Some(parent) = get_parent(&root_surface) {
root_surface = parent;
}
let output = self
.niri
.layout
.outputs()
.find(|o| {
let map = layer_map_for_output(o);
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
.is_some()
})
.cloned()
else {
return;
.cloned();
let Some(output) = output else {
return false;
};
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if surface == &root_surface {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<LayerSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
let mut map = layer_map_for_output(&output);
let mut map = layer_map_for_output(&output);
// Arrange the layers before sending the initial configure to respect any size the
// client may have sent.
map.arrange();
// arrange the layers before sending the initial configure
// to respect any size the client may have sent
map.arrange();
// send the initial configure if relevant
if !initial_configure_sent {
let layer = map
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
.unwrap();
let scale = output.current_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_scale_transform(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
}
}
+105 -23
View File
@@ -7,12 +7,15 @@ 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};
@@ -22,8 +25,8 @@ 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::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,
@@ -33,7 +36,7 @@ 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,
};
@@ -64,19 +67,22 @@ use smithay::{
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
use crate::niri::{ClientState, State};
use crate::niri::{ClientState, DndIcon, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::utils::{output_size, send_scale_transform};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management,
delegate_screencopy,
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;
type PointerFocus = WlSurface;
@@ -134,11 +140,66 @@ 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);
@@ -224,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();
}
@@ -387,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;
}
@@ -402,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);
}
}
@@ -550,16 +627,18 @@ impl XdgActivationHandler for State {
fn request_activation(
&mut self,
_token: XdgActivationToken,
token: XdgActivationToken,
token_data: XdgActivationTokenData,
surface: WlSurface,
) {
if token_data.timestamp.elapsed().as_secs() < 10 {
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);
}
}
}
@@ -580,3 +659,6 @@ impl OutputManagementHandler for State {
}
}
delegate_output_management!(State);
impl MutterX11InteropHandler for State {}
delegate_mutter_x11_interop!(State);
+257 -71
View File
@@ -1,5 +1,6 @@
use std::cell::Cell;
use calloop::Interest;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -17,26 +18,34 @@ 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, with_states, BufferAssignment, HookId, SurfaceAttributes,
add_blocker, add_pre_commit_hook, with_states, BufferAssignment, CompositorHandler as _,
HookId, SurfaceAttributes,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::selection::data_device::DnDGrab;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::{self, Layer};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use tracing::field::Empty;
use crate::input::move_grab::MoveGrab;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::input::touch_move_grab::TouchMoveGrab;
use crate::input::touch_resize_grab::TouchResizeGrab;
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::{get_monotonic_time, send_scale_transform, ResizeEdge};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -60,8 +69,94 @@ impl XdgShellHandler for State {
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
let wl_surface = surface.wl_surface();
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
pointer.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().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(
@@ -71,24 +166,39 @@ impl XdgShellHandler for State {
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
if pointer.has_grab(serial) {
if let Some(start_data) = pointer.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
}
}
}
}
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
if touch.has_grab(serial) {
if let Some(start_data) = touch.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
}
}
}
}
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
@@ -114,10 +224,8 @@ impl XdgShellHandler for State {
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.reset_window_height();
self.niri.layout.reset_window_height(Some(&window));
}
// FIXME: granular.
self.niri.queue_redraw_all();
@@ -125,14 +233,25 @@ impl XdgShellHandler for State {
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
if !self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
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(
@@ -258,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();
}
}
@@ -285,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);
}
}
@@ -329,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
@@ -413,7 +532,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = workspace_name
@@ -475,22 +594,32 @@ impl XdgShellHandler for State {
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: u64::from(mapped.id().get()),
id: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let transaction = Transaction::new();
let blocker = transaction.blocker();
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window);
.start_close_animation_for_window(renderer, &window, blocker);
});
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
// If this is the only instance, then this transaction will complete immediately, so no
// need to set the timer.
if !transaction.is_last() {
transaction.register_deadline_timer(&self.niri.event_loop);
}
if was_active {
self.maybe_warp_cursor_to_focus();
@@ -539,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();
}
}
@@ -552,7 +681,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();
}
}
@@ -607,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");
@@ -653,7 +770,12 @@ impl State {
rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|name| {
self.niri
.global_space
.outputs()
.find(|output| output_matches_name(output, name))
})
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
@@ -688,7 +810,7 @@ impl State {
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
@@ -741,7 +863,7 @@ impl State {
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
workspace_name: ws.and_then(|w| w.name().cloned()),
};
toplevel.send_configure();
@@ -770,16 +892,7 @@ 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();
@@ -999,16 +1112,25 @@ fn unconstrain_with_padding(
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 got_unmapped = {
let (got_unmapped, dmabuf, commit_serial) = with_states(surface, |states| {
let (got_unmapped, dmabuf) = {
let mut guard = states.cached_state.get::<SurfaceAttributes>();
matches!(guard.pending().buffer, Some(BufferAssignment::Removed))
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
@@ -1018,16 +1140,80 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
.lock()
.unwrap();
(got_unmapped, role.configure_serial)
(got_unmapped, dmabuf, role.configure_serial)
});
let animate = if let Some(serial) = commit_serial {
mapped.should_animate_commit(serial)
let mut transaction_for_dmabuf = None;
let mut animate = false;
if let Some(serial) = commit_serial {
if !span.is_disabled() {
span.record("serial", format!("{serial:?}"));
}
trace!("taking pending transaction");
if let Some(transaction) = mapped.take_pending_transaction(serial) {
// Transaction can be already completed if it ran past the deadline.
let disable = state.niri.config.borrow().debug.disable_transactions;
if !transaction.is_completed() && !disable {
// Register the deadline even if this is the last pending, since dmabuf
// rendering can still run over the deadline.
transaction.register_deadline_timer(&state.niri.event_loop);
let is_last = transaction.is_last();
// If this is the last transaction, we don't need to add a separate
// notification, because the transaction will complete in our dmabuf blocker
// callback, which already calls blocker_cleared(), or by the end of this
// function, in which case there would be no blocker in the first place.
if !is_last {
// Waiting for some other surface; register a notification and add a
// transaction blocker.
if let Some(client) = surface.client() {
transaction.add_notification(
state.niri.blocker_cleared_tx.clone(),
client.clone(),
);
add_blocker(surface, transaction.blocker());
}
}
// Delay dropping (and completing) the transaction until the dmabuf is ready.
// If there's no dmabuf, this will be dropped by the end of this pre-commit
// hook.
transaction_for_dmabuf = Some(transaction);
}
}
animate = mapped.should_animate_commit(serial);
} else {
error!("commit on a mapped surface without a configured serial");
false
};
if let Some((blocker, source)) =
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok())
{
if let Some(client) = surface.client() {
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
// This surface is now ready for the transaction.
drop(transaction_for_dmabuf.take());
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
trace!("added dmabuf blocker");
}
}
}
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
+535 -92
View File
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);
}
}
-1
View File
@@ -22,7 +22,6 @@ impl ResizeGrab {
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_resize_end(&self.window);
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
-1
View File
@@ -50,7 +50,6 @@ impl SpatialMovementGrab {
state.niri.queue_redraw(&output);
}
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
+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);
}
}
+141 -22
View File
@@ -1,6 +1,9 @@
use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, Socket, Transform,
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
};
use serde_json::json;
@@ -19,12 +22,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
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")?;
@@ -35,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,
};
@@ -114,11 +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() {
print_output(connector, output)?;
for (_name, output) in outputs.into_iter() {
print_output(output)?;
println!();
}
}
@@ -134,23 +144,30 @@ 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:?}");
@@ -163,7 +180,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
}
if let Some(output) = output {
print_output(output.name.clone(), output)?;
print_output(output)?;
} else {
println!("No output is focused.");
}
@@ -238,16 +255,94 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("{is_active}{idx}{name}");
}
}
Msg::KeyboardLayouts => {
let Response::KeyboardLayouts(response) = response else {
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let KeyboardLayouts { names, current_idx } = response;
let current_idx = usize::from(current_idx);
println!("Keyboard layouts:");
for (idx, name) in names.iter().enumerate() {
let is_active = if idx == current_idx { " * " } else { " " };
println!("{is_active}{idx} {name}");
}
}
Msg::EventStream => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
};
if !json {
println!("Started reading events.");
}
loop {
let event = read_event().context("error reading event from niri")?;
if json {
let event = serde_json::to_string(&event).context("error formatting event")?;
println!("{event}");
continue;
}
match event {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
println!(
"Workspace {workspace_id}: \
active window changed to {active_window_id:?}"
);
}
Event::WindowsChanged { windows } => {
println!("Windows changed: {windows:?}");
}
Event::WindowOpenedOrChanged { window } => {
println!("Window opened or changed: {window:?}");
}
Event::WindowClosed { id } => {
println!("Window closed: {id}");
}
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
}
}
}
}
Ok(())
}
fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
fn print_output(output: Output) -> anyhow::Result<()> {
let Output {
name,
make,
model,
serial,
physical_size,
modes,
current_mode,
@@ -256,7 +351,8 @@ fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
let serial = serial.as_deref().unwrap_or("Unknown");
println!(r#"Output "{make} {model} {serial}" ({name})"#);
if let Some(current) = current_mode {
let mode = *modes
@@ -336,3 +432,26 @@ fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
}
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)");
}
}
+386 -39
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::{OutputConfigChanged, Reply, Request, Response};
use smithay::desktop::Window;
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
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,6 +175,7 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let reply = match request {
Ok(request) => process(&ctx, request).await,
@@ -131,9 +188,50 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
}
}
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
}
Ok(())
}
@@ -146,24 +244,26 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
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) => {
@@ -185,7 +285,7 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.values()
.any(|o| o.name.eq_ignore_ascii_case(&output));
.any(|o| OutputName::from_ipc_output(o).matches(&output));
let response = if found {
OutputConfigChanged::Applied
} else {
@@ -199,16 +299,6 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Response::OutputConfigChanged(response)
}
Request::Workspaces => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
let workspaces = state.niri.layout.ipc_workspaces();
let _ = tx.send_blocking(workspaces);
});
let result = rx.recv().await;
let workspaces = result.map_err(|_| String::from("error getting workspace info"))?;
Response::Workspaces(workspaces)
}
Request::FocusedOutput => {
let (tx, rx) = async_channel::bounded(1);
ctx.event_loop.insert_idle(move |state| {
@@ -235,7 +325,264 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
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);
}
}
}
+72 -7
View File
@@ -12,6 +12,7 @@ use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::Texture;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{Blocker, BlockerState};
use crate::animation::Animation;
use crate::niri_render_elements;
@@ -21,6 +22,7 @@ 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 {
@@ -46,7 +48,7 @@ pub struct ClosingWindow {
blocked_out_buffer_offset: Point<f64, Logical>,
/// The closing animation.
anim: Animation,
anim_state: AnimationState,
/// Random seed for the shader.
random_seed: f32,
@@ -59,6 +61,29 @@ niri_render_elements! {
}
}
#[derive(Debug)]
enum AnimationState {
Waiting {
/// Blocker for a transaction before starting the animation.
blocker: TransactionBlocker,
anim: Animation,
},
Animating(Animation),
}
impl AnimationState {
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
if blocker.state() == BlockerState::Pending {
Self::Waiting { blocker, anim }
} else {
// This actually doesn't normally happen because the window is removed only after the
// closing animation is created. Though, it does happen with disable-transactions debug
// flag.
Self::Animating(anim)
}
}
}
impl ClosingWindow {
pub fn new<E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
@@ -66,6 +91,7 @@ impl ClosingWindow {
scale: Scale<f64>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
blocker: TransactionBlocker,
anim: Animation,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
@@ -107,17 +133,29 @@ impl ClosingWindow {
pos,
buffer_offset,
blocked_out_buffer_offset,
anim,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
})
}
pub fn advance_animations(&mut self, current_time: Duration) {
self.anim.set_current_time(current_time);
match &mut self.anim_state {
AnimationState::Waiting { blocker, anim } => {
if blocker.state() != BlockerState::Pending {
let mut anim = anim.restarted(0., 1., 0.);
anim.set_current_time(current_time);
self.anim_state = AnimationState::Animating(anim);
}
}
AnimationState::Animating(anim) => anim.set_current_time(current_time),
}
}
pub fn are_animations_ongoing(&self) -> bool {
!self.anim.is_done()
match &self.anim_state {
AnimationState::Waiting { .. } => true,
AnimationState::Animating(anim) => !anim.is_done(),
}
}
pub fn render(
@@ -127,15 +165,42 @@ impl ClosingWindow {
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let progress = self.anim.value();
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else {
(&self.buffer, self.buffer_offset)
};
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);
+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)
}
}
+1816 -291
View File
File diff suppressed because it is too large Load Diff
+265 -240
View File
@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::utils::{
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle};
use super::tile::Tile;
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
@@ -19,7 +20,8 @@ use crate::input::swipe_tracker::SwipeTracker;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::{output_size, to_physical_precise_round, ResizeEdge};
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, round_logical_in_physical, ResizeEdge};
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
@@ -32,17 +34,19 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
pub output: Output,
pub(super) output: Output,
/// Cached name of the output.
output_name: String,
// Must always contain at least one.
pub workspaces: Vec<Workspace<W>>,
pub(super) workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
pub(super) active_workspace_idx: usize,
/// ID of the previously active workspace.
pub previous_workspace_id: Option<WorkspaceId>,
pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
pub(super) workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
pub options: Rc<Options>,
pub(super) options: Rc<Options>,
}
#[derive(Debug)]
@@ -56,7 +60,7 @@ pub struct WorkspaceSwitchGesture {
/// Index of the workspace where the gesture was started.
center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
pub(super) current_idx: f64,
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
@@ -92,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,
@@ -101,6 +106,18 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn output(&self) -> &Output {
&self.output
}
pub fn output_name(&self) -> &String {
&self.output_name
}
pub fn active_workspace_idx(&self) -> usize {
self.active_workspace_idx
}
pub fn active_workspace_ref(&self) -> &Workspace<W> {
&self.workspaces[self.active_workspace_idx]
}
@@ -125,6 +142,14 @@ impl<W: LayoutElement> Monitor<W> {
&mut self.workspaces[self.active_workspace_idx]
}
pub fn windows(&self) -> impl Iterator<Item = &W> {
self.workspaces.iter().flat_map(|ws| ws.windows())
}
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
@@ -159,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);
@@ -193,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);
@@ -214,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());
@@ -298,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();
}
@@ -387,7 +457,7 @@ impl<W: LayoutElement> Monitor<W> {
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();
self.focus_right();
} else {
workspace.focus_up();
}
@@ -439,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) {
@@ -462,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 {
@@ -480,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) {
@@ -512,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);
}
@@ -529,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);
}
@@ -546,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) {
@@ -571,13 +675,8 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.iter().position(|w| w.id() == id)
}
pub fn switch_workspace(&mut self, idx: usize, animate: bool) {
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
if !animate {
self.workspace_switch = None;
self.clean_up_workspaces();
}
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
@@ -585,16 +684,16 @@ impl<W: LayoutElement> Monitor<W> {
if idx == self.active_workspace_idx {
if let Some(prev_idx) = self.previous_workspace_idx() {
self.switch_workspace(prev_idx, false);
self.switch_workspace(prev_idx);
}
} else {
self.switch_workspace(idx, false);
self.switch_workspace(idx);
}
}
pub fn switch_workspace_previous(&mut self) {
if let Some(idx) = self.previous_workspace_idx() {
self.switch_workspace(idx, false);
self.switch_workspace(idx);
}
}
@@ -634,7 +733,7 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn are_animations_ongoing(&self) -> bool {
pub(super) fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())
@@ -709,14 +808,6 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().set_column_width(change);
}
pub fn set_window_height(&mut self, change: SizeChange) {
self.active_workspace().set_window_height(change);
}
pub fn reset_window_height(&mut self) {
self.active_workspace().reset_window_height();
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
@@ -780,90 +871,75 @@ impl<W: LayoutElement> Monitor<W> {
Some(rect)
}
pub fn workspaces_with_render_positions(
&self,
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
let mut first = None;
let mut second = None;
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
if after_idx >= 0. && before_idx < self.workspaces.len() as f64 {
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
let offset =
round_logical_in_physical(scale, (render_idx - before_idx) * size.h);
// Ceil the height in physical pixels.
let height = (size.h * scale).ceil() / scale;
if before_idx >= 0. {
let before_idx = before_idx as usize;
let before_offset = Point::from((0., -offset));
first = Some((&self.workspaces[before_idx], before_offset));
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
let after_offset = Point::from((0., -offset + height));
second = Some((&self.workspaces[after_idx], after_offset));
}
}
}
None => {
first = Some((
&self.workspaces[self.active_workspace_idx],
Point::from((0., 0.)),
));
}
}
first.into_iter().chain(second)
}
pub fn workspace_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&Workspace<W>, Point<f64, Logical>)> {
let size = output_size(&self.output);
let (ws, bounds) = self
.workspaces_with_render_positions()
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
Some((ws, bounds.loc))
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<f64, Logical>>)> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output).to_f64();
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = (render_idx - before_idx) * size.h;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < size.h - offset {
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0., offset)))
} else {
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0., -size.h + offset)))
};
let ws = &self.workspaces[idx];
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset)?;
Some((win, win_pos.map(|p| p - ws_offset)))
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.window_under(pos_within_output)
}
}
let (ws, offset) = self.workspace_under(pos_within_output)?;
let (win, win_pos) = ws.window_under(pos_within_output - offset)?;
Some((win, win_pos.map(|p| p + offset)))
}
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = (render_idx - before_idx) * size.h;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < size.h - offset {
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0., offset)))
} else {
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0., -size.h + offset)))
};
let ws = &self.workspaces[idx];
ws.resize_edges_under(pos_within_output + ws_offset)
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.resize_edges_under(pos_within_output)
}
}
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 {
@@ -876,103 +952,52 @@ 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 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;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return vec![];
}
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
let after = self.workspaces[after_idx].render_elements(renderer, target);
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
scale,
// HACK: crop to infinite bounds for all sides except the side
// where the workspaces join,
// otherwise it will cut pixel shaders and mess up
// the coordinate space.
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
Point::from((0., -offset + size.h)).to_physical_precise_round(scale),
Relocate::Relative,
))
});
if before_idx < 0. {
return after.collect();
}
Some(after)
} else {
None
};
let before_idx = before_idx as usize;
let before = self.workspaces[before_idx].render_elements(renderer, target);
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
scale,
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, to_physical_precise_round(scale, size.h)),
),
)?,
Point::from((0., -offset)).to_physical_precise_round(scale),
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,
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, is_touchpad: bool) {
+25 -7
View File
@@ -23,6 +23,7 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::utils::transaction::Transaction;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -63,6 +64,9 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
/// Snapshot of the last render for use in the close animation.
unmap_snapshot: Option<TileRenderSnapshot>,
@@ -73,7 +77,7 @@ pub struct Tile<W: LayoutElement> {
scale: f64,
/// Configurable properties of the layout.
pub options: Rc<Options>,
pub(super) options: Rc<Options>,
}
niri_render_elements! {
@@ -89,7 +93,7 @@ niri_render_elements! {
}
}
type TileRenderSnapshot =
pub type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
@@ -122,6 +126,7 @@ impl<W: LayoutElement> Tile<W> {
resize_animation: None,
move_x_animation: None,
move_y_animation: None,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
scale,
@@ -304,6 +309,8 @@ impl<W: LayoutElement> Tile<W> {
offset.y += move_.from * move_.anim.value();
}
offset += self.interactive_move_offset;
offset
}
@@ -363,6 +370,11 @@ impl<W: LayoutElement> Tile<W> {
});
}
pub fn stop_move_animations(&mut self) {
self.move_x_animation = None;
self.move_y_animation = None;
}
pub fn window(&self) -> &W {
&self.window
}
@@ -380,7 +392,7 @@ impl<W: LayoutElement> Tile<W> {
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<f64> {
pub fn effective_border_width(&self) -> Option<f64> {
if self.is_fullscreen {
return None;
}
@@ -503,7 +515,12 @@ impl<W: LayoutElement> Tile<W> {
activation_region.contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<f64, Logical>, animate: bool) {
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
animate: bool,
transaction: Option<Transaction>,
) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
@@ -514,7 +531,8 @@ impl<W: LayoutElement> Tile<W> {
// The size request has to be i32 unfortunately, due to Wayland. We floor here instead of
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window.request_size(size.to_i32_floor(), animate);
self.window
.request_size(size.to_i32_floor(), animate, transaction);
}
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
@@ -758,8 +776,8 @@ impl<W: LayoutElement> Tile<W> {
geo.size,
Rectangle::from_loc_and_size((0., 0.), geo.size),
GradientInterpolation::default(),
Color::from_array_premul(elem.color()),
Color::from_array_premul(elem.color()),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_loc_and_size((0., 0.), geo.size),
0.,
+1167 -524
View File
File diff suppressed because it is too large Load Diff
+85 -61
View File
@@ -24,6 +24,7 @@ use niri::utils::spawning::{
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;
@@ -32,6 +33,11 @@ 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() {
@@ -90,10 +96,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Sub::Validate { config } => {
tracy_client::Client::start();
let path = config
.or_else(env_config_path)
.or_else(default_config_path)
.expect("error getting config path");
let (path, _, _) = config_path(config);
Config::load(&path)?;
info!("config is valid");
return Ok(());
@@ -114,56 +117,50 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load the config.
let mut config_created = false;
let path = cli.config.or_else(env_config_path);
let (path, watch_path, create_default) = config_path(cli.config);
env::remove_var("NIRI_CONFIG");
let path = path.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
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();
@@ -200,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());
}
@@ -220,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.
@@ -273,7 +270,7 @@ fn import_environment() {
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
SOCKET_PATH_ENV,
]
.join(" ");
@@ -335,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()?,
+465 -225
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);
});
}
}
+3
View File
@@ -1,4 +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);
};
}
+17 -9
View File
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::mem;
use niri_config::FloatOrInt;
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,
@@ -403,12 +403,13 @@ where
return;
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&current_config.name)
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = false;
@@ -452,12 +453,13 @@ where
);
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&current_config.name)
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = true;
@@ -693,9 +695,9 @@ where
new_config.scale = Some(FloatOrInt(scale));
}
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
let enabled = match state {
WEnum::Value(AdaptiveSyncState::Enabled) => true,
WEnum::Value(AdaptiveSyncState::Disabled) => false,
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(
@@ -705,7 +707,7 @@ where
return;
}
};
new_config.variable_refresh_rate = enabled;
new_config.variable_refresh_rate = vrr;
}
_ => unreachable!(),
}
@@ -841,6 +843,7 @@ fn send_new_head<D>(
.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()) {
@@ -877,6 +880,11 @@ fn send_new_head<D>(
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 {
+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");
}
}
}
}
+110 -13
View File
@@ -8,6 +8,8 @@ use std::rc::Rc;
use std::time::Duration;
use anyhow::Context as _;
use calloop::timer::{TimeoutAction, Timer};
use calloop::RegistrationToken;
use pipewire::context::Context;
use pipewire::core::Core;
use pipewire::main_loop::MainLoop;
@@ -31,9 +33,10 @@ use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::{Format, Fourcc};
use smithay::backend::drm::DrmDeviceFd;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::WeakOutput;
use smithay::output::{Output, OutputModeSource, WeakOutput};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
@@ -43,6 +46,10 @@ use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State;
use crate::render_helpers::render_to_dmabuf;
use crate::utils::get_monotonic_time;
// Give a 0.1 ms allowance for presentation time errors.
const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
pub struct PipeWire {
_context: Context,
@@ -69,6 +76,7 @@ pub struct Cast {
pub last_frame_time: Duration,
min_time_between_frames: Rc<Cell<Duration>>,
dmabufs: Rc<RefCell<HashMap<i64, Dmabuf>>>,
scheduled_redraw: Option<RegistrationToken>,
}
#[derive(Debug)]
@@ -87,6 +95,8 @@ pub enum CastState {
alpha: bool,
modifier: Modifier,
plane_count: i32,
// Lazily-initialized to keep the initialization to a single place.
damage_tracker: Option<OutputDamageTracker>,
},
}
@@ -312,10 +322,9 @@ impl PipeWire {
};
let max_frame_rate = format.max_framerate();
// Subtract 0.5 ms to improve edge cases when equal to refresh rate.
let min_frame_time = Duration::from_secs_f64(
max_frame_rate.denom as f64 / max_frame_rate.num as f64,
) - Duration::from_micros(500);
let min_frame_time = Duration::from_micros(
1_000_000 * u64::from(max_frame_rate.denom) / u64::from(max_frame_rate.num),
);
min_time_between_frames.set(min_frame_time);
let object = pod.as_object().unwrap();
@@ -417,6 +426,7 @@ impl PipeWire {
alpha,
modifier,
plane_count,
..
} if *alpha == format_has_alpha
&& *modifier == Modifier::from(format.modifier()) =>
{
@@ -425,6 +435,13 @@ impl PipeWire {
let modifier = *modifier;
let plane_count = *plane_count;
let damage_tracker =
if let CastState::Ready { damage_tracker, .. } = &mut *state {
damage_tracker.take()
} else {
None
};
debug!("pw stream: moving to ready state");
*state = CastState::Ready {
@@ -432,6 +449,7 @@ impl PipeWire {
alpha,
modifier,
plane_count,
damage_tracker,
};
plane_count
@@ -464,6 +482,7 @@ impl PipeWire {
alpha: format_has_alpha,
modifier,
plane_count: plane_count as i32,
damage_tracker: None,
};
plane_count as i32
@@ -638,6 +657,7 @@ impl PipeWire {
last_frame_time: Duration::ZERO,
min_time_between_frames,
dmabufs,
scheduled_redraw: None,
};
Ok(cast)
}
@@ -698,13 +718,13 @@ impl Cast {
Ok(())
}
pub fn should_skip_frame(&self, target_frame_time: Duration) -> bool {
fn compute_extra_delay(&self, target_frame_time: Duration) -> Duration {
let last = self.last_frame_time;
let min = self.min_time_between_frames.get();
if last.is_zero() {
trace!(?target_frame_time, ?last, "last is zero, recording");
return false;
return Duration::ZERO;
}
if target_frame_time < last {
@@ -714,29 +734,106 @@ impl Cast {
?last,
"target frame time is below last, did it overflow or did we mispredict?"
);
return false;
return Duration::ZERO;
}
let diff = target_frame_time - last;
if diff < min {
let delay = min - diff;
trace!(
?target_frame_time,
?last,
"skipping frame because it is too soon: diff={diff:?} < min={min:?}",
"frame is too soon: min={min:?}, delay={:?}",
delay
);
return true;
return delay;
} else {
trace!("overshoot={:?}", diff - min);
}
false
Duration::ZERO
}
fn schedule_redraw(
&mut self,
event_loop: &LoopHandle<'static, State>,
output: Output,
target_time: Duration,
) {
if self.scheduled_redraw.is_some() {
return;
}
let now = get_monotonic_time();
let duration = target_time.saturating_sub(now);
let timer = Timer::from_duration(duration);
let token = event_loop
.insert_source(timer, move |_, _, state| {
state.niri.queue_redraw(&output);
TimeoutAction::Drop
})
.unwrap();
self.scheduled_redraw = Some(token);
}
fn remove_scheduled_redraw(&mut self, event_loop: &LoopHandle<'static, State>) {
if let Some(token) = self.scheduled_redraw.take() {
event_loop.remove(token);
}
}
/// Checks whether this frame should be skipped because it's too soon.
///
/// If the frame should be skipped, schedules a redraw and returns `true`. Otherwise, removes a
/// scheduled redraw, if any, and returns `false`.
///
/// When this method returns `false`, the calling code is assumed to follow up with
/// [`Cast::dequeue_buffer_and_render()`].
pub fn check_time_and_schedule(
&mut self,
event_loop: &LoopHandle<'static, State>,
output: &Output,
target_frame_time: Duration,
) -> bool {
let delay = self.compute_extra_delay(target_frame_time);
if delay >= CAST_DELAY_ALLOWANCE {
trace!("delay >= allowance, scheduling redraw");
self.schedule_redraw(event_loop, output.clone(), target_frame_time + delay);
true
} else {
self.remove_scheduled_redraw(event_loop);
false
}
}
pub fn dequeue_buffer_and_render(
&mut self,
renderer: &mut GlesRenderer,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
elements: &[impl RenderElement<GlesRenderer>],
size: Size<i32, Physical>,
scale: Scale<f64>,
) -> bool {
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
error!("cast must be in Ready state to render");
return false;
};
let damage_tracker = damage_tracker
.get_or_insert_with(|| OutputDamageTracker::new(size, scale, Transform::Normal));
// Size change will drop the damage tracker, but scale change won't, so check it here.
let OutputModeSource::Static { scale: t_scale, .. } = damage_tracker.mode() else {
unreachable!();
};
if *t_scale != scale {
*damage_tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
}
let (damage, _states) = damage_tracker.damage_output(1, elements).unwrap();
if damage.is_none() {
trace!("no damage, skipping frame");
return false;
}
let mut buffer = match self.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
@@ -754,7 +851,7 @@ impl Cast {
size,
scale,
Transform::Normal,
elements,
elements.iter().rev(),
) {
warn!("error rendering to dmabuf: {err:?}");
return false;
+2 -2
View File
@@ -8,7 +8,7 @@ use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement
use smithay::backend::renderer::element::{Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::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};
@@ -302,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 {
+32 -58
View File
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::ffi::CString;
use std::rc::Rc;
use glam::{Mat3, Vec2};
@@ -76,38 +76,31 @@ unsafe fn compile_program(
let debug_program =
unsafe { link_program(gl, include_str!("shaders/texture.vert"), &debug_shader)? };
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
let size = CStr::from_bytes_with_nul(b"niri_size\0").expect("NULL terminated");
let scale = CStr::from_bytes_with_nul(b"niri_scale\0").expect("NULL terminated");
let alpha = CStr::from_bytes_with_nul(b"niri_alpha\0").expect("NULL terminated");
let tint = CStr::from_bytes_with_nul(b"niri_tint\0").expect("NULL terminated");
let vert = c"vert";
let vert_position = c"vert_position";
let matrix = c"matrix";
let tex_matrix = c"tex_matrix";
let size = c"niri_size";
let scale = c"niri_scale";
let alpha = c"niri_alpha";
let tint = c"niri_tint";
Ok(ShaderProgram(Rc::new(ShaderProgramInner {
normal: ShaderProgramInternal {
program,
uniform_matrix: gl
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
uniform_size: gl
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
uniform_scale: gl
.GetUniformLocation(program, scale.as_ptr() as *const ffi::types::GLchar),
uniform_alpha: gl
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr()),
uniform_tex_matrix: gl.GetUniformLocation(program, tex_matrix.as_ptr()),
uniform_size: gl.GetUniformLocation(program, size.as_ptr()),
uniform_scale: gl.GetUniformLocation(program, scale.as_ptr()),
uniform_alpha: gl.GetUniformLocation(program, alpha.as_ptr()),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
attrib_vert_position: gl.GetAttribLocation(program, vert_position.as_ptr()),
additional_uniforms: additional_uniforms
.iter()
.map(|uniform| {
let name =
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
let location = gl.GetUniformLocation(program, name.as_ptr());
(
uniform.name.clone().into_owned(),
UniformDesc {
@@ -121,41 +114,26 @@ unsafe fn compile_program(
.iter()
.map(|name_| {
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
let location =
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
let location = gl.GetUniformLocation(program, name.as_ptr());
(name_.to_string(), location)
})
.collect(),
},
debug: ShaderProgramInternal {
program: debug_program,
uniform_matrix: gl
.GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl.GetUniformLocation(
debug_program,
tex_matrix.as_ptr() as *const ffi::types::GLchar,
),
uniform_size: gl
.GetUniformLocation(debug_program, size.as_ptr() as *const ffi::types::GLchar),
uniform_scale: gl
.GetUniformLocation(debug_program, scale.as_ptr() as *const ffi::types::GLchar),
uniform_alpha: gl
.GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar),
attrib_vert: gl
.GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl.GetAttribLocation(
debug_program,
vert_position.as_ptr() as *const ffi::types::GLchar,
),
uniform_matrix: gl.GetUniformLocation(debug_program, matrix.as_ptr()),
uniform_tex_matrix: gl.GetUniformLocation(debug_program, tex_matrix.as_ptr()),
uniform_size: gl.GetUniformLocation(debug_program, size.as_ptr()),
uniform_scale: gl.GetUniformLocation(debug_program, scale.as_ptr()),
uniform_alpha: gl.GetUniformLocation(debug_program, alpha.as_ptr()),
attrib_vert: gl.GetAttribLocation(debug_program, vert.as_ptr()),
attrib_vert_position: gl.GetAttribLocation(debug_program, vert_position.as_ptr()),
additional_uniforms: additional_uniforms
.iter()
.map(|uniform| {
let name =
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
let location = gl.GetUniformLocation(
debug_program,
name.as_ptr() as *const ffi::types::GLchar,
);
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
(
uniform.name.clone().into_owned(),
UniformDesc {
@@ -169,16 +147,12 @@ unsafe fn compile_program(
.iter()
.map(|name_| {
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
let location = gl.GetUniformLocation(
debug_program,
name.as_ptr() as *const ffi::types::GLchar,
);
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
(name_.to_string(), location)
})
.collect(),
},
uniform_tint: gl
.GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar),
uniform_tint: gl.GetUniformLocation(debug_program, tint.as_ptr()),
})))
}
@@ -211,7 +185,7 @@ impl ShaderRenderElement {
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
scale: f32,
alpha: f32,
uniforms: Vec<Uniform<'_>>,
additional_uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
kind: Kind,
) -> Self {
@@ -223,7 +197,7 @@ impl ShaderRenderElement {
opaque_regions: opaque_regions.unwrap_or_default(),
scale,
alpha,
additional_uniforms: uniforms.into_iter().map(|u| u.into_owned()).collect(),
additional_uniforms,
textures,
kind,
}
@@ -253,13 +227,13 @@ impl ShaderRenderElement {
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
scale: f32,
uniforms: Vec<Uniform<'_>>,
uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
) {
self.area.size = size;
self.opaque_regions = opaque_regions.unwrap_or_default();
self.scale = scale;
self.additional_uniforms = uniforms.into_iter().map(|u| u.into_owned()).collect();
self.additional_uniforms = uniforms;
self.textures = textures;
self.commit_counter.increment();
+15 -18
View File
@@ -1,6 +1,6 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::{Frame as _, Renderer};
use smithay::backend::renderer::{Color32F, Frame as _, Renderer};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
/// Smithay's solid color buffer, but with fractional scale.
@@ -9,7 +9,7 @@ pub struct SolidColorBuffer {
id: Id,
size: Size<f64, Logical>,
commit: CommitCounter,
color: [f32; 4],
color: Color32F,
}
/// Render element for a [`SolidColorBuffer`].
@@ -18,7 +18,7 @@ pub struct SolidColorRenderElement {
id: Id,
geometry: Rectangle<f64, Logical>,
commit: CommitCounter,
color: [f32; 4],
color: Color32F,
kind: Kind,
}
@@ -34,10 +34,10 @@ impl Default for SolidColorBuffer {
}
impl SolidColorBuffer {
pub fn new(size: impl Into<Size<f64, Logical>>, color: [f32; 4]) -> Self {
pub fn new(size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) -> Self {
SolidColorBuffer {
id: Id::new(),
color,
color: color.into(),
commit: CommitCounter::default(),
size: size.into(),
}
@@ -51,15 +51,17 @@ impl SolidColorBuffer {
}
}
pub fn set_color(&mut self, color: [f32; 4]) {
pub fn set_color(&mut self, color: impl Into<Color32F>) {
let color = color.into();
if color != self.color {
self.color = color;
self.commit.increment();
}
}
pub fn update(&mut self, size: impl Into<Size<f64, Logical>>, color: [f32; 4]) {
pub fn update(&mut self, size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) {
let size = size.into();
let color = color.into();
if size != self.size || color != self.color {
self.size = size;
self.color = color;
@@ -67,7 +69,7 @@ impl SolidColorBuffer {
}
}
pub fn color(&self) -> [f32; 4] {
pub fn color(&self) -> Color32F {
self.color
}
@@ -84,12 +86,7 @@ impl SolidColorRenderElement {
kind: Kind,
) -> Self {
let geo = Rectangle::from_loc_and_size(location, buffer.size());
let color = [
buffer.color[0] * alpha,
buffer.color[1] * alpha,
buffer.color[2] * alpha,
buffer.color[3] * alpha,
];
let color = buffer.color * alpha;
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
}
@@ -97,7 +94,7 @@ impl SolidColorRenderElement {
id: Id,
geometry: Rectangle<f64, Logical>,
commit: CommitCounter,
color: [f32; 4],
color: Color32F,
kind: Kind,
) -> Self {
SolidColorRenderElement {
@@ -109,7 +106,7 @@ impl SolidColorRenderElement {
}
}
pub fn color(&self) -> [f32; 4] {
pub fn color(&self) -> Color32F {
self.color
}
@@ -136,7 +133,7 @@ impl Element for SolidColorRenderElement {
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if self.color[3] == 1f32 {
if self.color.is_opaque() {
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
.to_physical_precise_down(scale);
OpaqueRegions::from_slice(&[rect])
@@ -146,7 +143,7 @@ impl Element for SolidColorRenderElement {
}
fn alpha(&self) -> f32 {
self.color[3]
self.color.a()
}
fn kind(&self) -> Kind {
+12
View File
@@ -92,6 +92,18 @@ impl<T> TextureBuffer<T> {
pub fn texture_scale(&self) -> Scale<f64> {
self.scale
}
pub fn set_texture_scale(&mut self, scale: impl Into<Scale<f64>>) {
self.scale = scale.into();
}
pub fn texture_transform(&self) -> Transform {
self.transform
}
pub fn set_texture_transform(&mut self, transform: Transform) {
self.transform = transform;
}
}
impl<T: Texture> TextureBuffer<T> {
+2 -1
View File
@@ -60,7 +60,8 @@ impl ConfigErrorNotification {
Animation::new(from, to, 0., c.animations.config_notification_open_close.0)
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
pub fn show_created(&mut self, created_path: PathBuf) {
let created_path = Some(created_path);
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
+9
View File
@@ -2,6 +2,7 @@ use std::time::Duration;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::GlesTexture;
use smithay::utils::{Scale, Transform};
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
@@ -43,6 +44,14 @@ impl ScreenTransition {
self.alpha == 0.
}
pub fn update_render_elements(&mut self, scale: Scale<f64>, transform: Transform) {
// These textures should remain full-screen, even if scale or transform changes.
for buffer in &mut self.from_texture {
buffer.set_texture_scale(scale);
buffer.set_texture_transform(transform);
}
}
pub fn render(&self, target: RenderTarget) -> PrimaryGpuTextureRenderElement {
let idx = match target {
RenderTarget::Output => 0,
+5 -8
View File
@@ -1,11 +1,8 @@
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::atomic::{AtomicU64, Ordering};
/// Counter that returns unique IDs.
///
/// Under the hood it uses a `u32` that will eventually wrap around. When incrementing it once a
/// second, it will wrap around after about 136 years.
pub struct IdCounter {
value: AtomicU32,
value: AtomicU64,
}
impl IdCounter {
@@ -13,12 +10,12 @@ impl IdCounter {
Self {
// Start from 1 to reduce the possibility that some other code that uses these IDs will
// get confused.
value: AtomicU32::new(1),
value: AtomicU64::new(1),
}
}
pub fn next(&self) -> u32 {
self.value.fetch_add(1, Ordering::SeqCst)
pub fn next(&self) -> u64 {
self.value.fetch_add(1, Ordering::Relaxed)
}
}
+31 -2
View File
@@ -10,19 +10,23 @@ use anyhow::{ensure, Context};
use bitflags::bitflags;
use directories::UserDirs;
use git_version::git_version;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::input::pointer::CursorIcon;
use smithay::output::{self, Output};
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Coordinate, Logical, Point, Rectangle, Size, Transform};
use smithay::wayland::compositor::{send_surface_state, SurfaceData};
use smithay::wayland::compositor::{send_surface_state, with_states, SurfaceData};
use smithay::wayland::fractional_scale::with_fractional_scale;
use smithay::wayland::shell::xdg::{
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
pub mod id;
pub mod scale;
pub mod spawning;
pub mod transaction;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
@@ -215,6 +219,31 @@ pub fn write_png_rgba8(
writer.write_image_data(pixels)
}
pub fn output_matches_name(output: &Output, target: &str) -> bool {
let name = output.user_data().get::<OutputName>().unwrap();
name.matches(target)
}
pub fn is_laptop_panel(connector: &str) -> bool {
matches!(connector.get(..4), Some("eDP-" | "LVDS" | "DSI-"))
}
pub fn with_toplevel_role<T>(
toplevel: &ToplevelSurface,
f: impl FnOnce(&mut XdgToplevelSurfaceRoleAttributes) -> T,
) -> T {
with_states(toplevel.wl_surface(), |states| {
let mut role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
f(&mut role)
})
}
#[cfg(feature = "dbus")]
pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
let mut notification = notify_rust::Notification::new();
+191
View File
@@ -0,0 +1,191 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, Instant};
use atomic::Ordering;
use calloop::ping::{make_ping, Ping};
use calloop::timer::{TimeoutAction, Timer};
use calloop::LoopHandle;
use smithay::reexports::wayland_server::Client;
use smithay::wayland::compositor::{Blocker, BlockerState};
/// Default time limit, after which the transaction completes.
///
/// Serves to avoid hanging when a client fails to respond to a configure promptly.
const TIME_LIMIT: Duration = Duration::from_millis(300);
/// Transaction between Wayland clients.
///
/// How to use it:
/// 1. Create a transaction with [`Transaction::new()`].
/// 2. Clone it as many times as you need.
/// 3. Before adding the transaction as a commit blocker, remember to call
/// [`Transaction::add_notification()`] to receive a notification when the transaction completes.
/// 4. Before adding the transaction as a commit blocker, remember to call
/// [`Transaction::register_deadline_timer()`] to make sure the transaction completes when
/// reaching the deadline.
/// 5. In your surface pre-commit handler, if the transaction corresponding to that commit isn't
/// ready, get a blocker with [`Transaction::blocker()`] and add it to the surface.
#[derive(Debug, Clone)]
pub struct Transaction {
inner: Arc<Inner>,
deadline: Rc<RefCell<Deadline>>,
}
/// Blocker for a [`Transaction`].
#[derive(Debug)]
pub struct TransactionBlocker(Weak<Inner>);
#[derive(Debug)]
enum Deadline {
NotRegistered(Instant),
Registered { remove: Ping },
}
#[derive(Debug)]
struct Inner {
/// Whether the transaction is completed.
completed: AtomicBool,
/// Notifications to send out upon completing the transaction.
notifications: Mutex<Option<(Sender<Client>, Vec<Client>)>>,
}
impl Transaction {
/// Creates a new transaction.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
inner: Arc::new(Inner::new()),
deadline: Rc::new(RefCell::new(Deadline::NotRegistered(
Instant::now() + TIME_LIMIT,
))),
}
}
/// Gets a blocker for this transaction.
pub fn blocker(&self) -> TransactionBlocker {
trace!(transaction = ?Arc::as_ptr(&self.inner), "generating blocker");
TransactionBlocker(Arc::downgrade(&self.inner))
}
/// Adds a notification for when this transaction completes.
pub fn add_notification(&self, sender: Sender<Client>, client: Client) {
if self.is_completed() {
error!("tried to add notification to a completed transaction");
return;
}
let mut guard = self.inner.notifications.lock().unwrap();
guard.get_or_insert((sender, Vec::new())).1.push(client);
}
/// Registers this transaction's deadline timer on an event loop.
pub fn register_deadline_timer<T: 'static>(&self, event_loop: &LoopHandle<'static, T>) {
let mut cell = self.deadline.borrow_mut();
if let Deadline::NotRegistered(deadline) = *cell {
let timer = Timer::from_deadline(deadline);
let inner = Arc::downgrade(&self.inner);
let token = event_loop
.insert_source(timer, move |_, _, _| {
let _span = trace_span!("deadline timer", transaction = ?Weak::as_ptr(&inner))
.entered();
if let Some(inner) = inner.upgrade() {
trace!("deadline reached, completing transaction");
inner.complete();
} else {
// We should remove the timer automatically. But this callback can still
// just happen to run while the ping callback is scheduled, leading to this
// branch being legitimately taken.
trace!("transaction completed without removing the timer");
}
TimeoutAction::Drop
})
.unwrap();
// Add a ping source that will be used to remove the timer automatically.
let (ping, source) = make_ping().unwrap();
let loop_handle = event_loop.clone();
event_loop
.insert_source(source, move |_, _, _| {
loop_handle.remove(token);
})
.unwrap();
*cell = Deadline::Registered { remove: ping };
}
}
/// Returns whether this transaction has already completed.
pub fn is_completed(&self) -> bool {
self.inner.is_completed()
}
/// Returns whether this is the last instance of this transaction.
pub fn is_last(&self) -> bool {
Arc::strong_count(&self.inner) == 1
}
}
impl Drop for Transaction {
fn drop(&mut self) {
let _span = trace_span!("drop", transaction = ?Arc::as_ptr(&self.inner)).entered();
if self.is_last() {
// If this was the last transaction, complete it.
trace!("last transaction dropped, completing");
self.inner.complete();
// Also remove the timer.
if let Deadline::Registered { remove } = &*self.deadline.borrow() {
remove.ping();
};
}
}
}
impl TransactionBlocker {
pub fn completed() -> Self {
Self(Weak::new())
}
}
impl Blocker for TransactionBlocker {
fn state(&self) -> BlockerState {
if self.0.upgrade().map_or(true, |x| x.is_completed()) {
BlockerState::Released
} else {
BlockerState::Pending
}
}
}
impl Inner {
fn new() -> Self {
Self {
completed: AtomicBool::new(false),
notifications: Mutex::new(None),
}
}
fn is_completed(&self) -> bool {
self.completed.load(Ordering::Relaxed)
}
fn complete(&self) {
self.completed.store(true, Ordering::Relaxed);
let mut guard = self.notifications.lock().unwrap();
if let Some((sender, clients)) = guard.take() {
for client in clients {
if let Err(err) = sender.send(client) {
warn!("error sending blocker notification: {err:?}");
};
}
}
}
}
+136 -12
View File
@@ -12,6 +12,7 @@ use smithay::output::{self, Output};
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Resource as _;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use smithay::wayland::compositor::{remove_pre_commit_hook, with_states, HookId};
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
@@ -19,7 +20,8 @@ use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use super::{ResolvedWindowRules, WindowRef};
use crate::handlers::KdeDecorationsModeState;
use crate::layout::{
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot,
};
use crate::niri::WindowOffscreenId;
use crate::niri_render_elements;
@@ -30,7 +32,8 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme
use crate::render_helpers::surface::render_snapshot_from_surface_tree;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
use crate::utils::id::IdCounter;
use crate::utils::{send_scale_transform, ResizeEdge};
use crate::utils::transaction::Transaction;
use crate::utils::{send_scale_transform, with_toplevel_role, ResizeEdge};
#[derive(Debug)]
pub struct Mapped {
@@ -69,6 +72,12 @@ pub struct Mapped {
/// Snapshot right before an animated commit.
animation_snapshot: Option<LayoutElementRenderSnapshot>,
/// Transaction that the next configure should take part in, if any.
transaction_for_next_configure: Option<Transaction>,
/// Pending transactions that have not been added as blockers for this window yet.
pending_transactions: Vec<(Serial, Transaction)>,
/// State of an ongoing interactive resize.
interactive_resize: Option<InteractiveResize>,
@@ -89,14 +98,14 @@ niri_render_elements! {
static MAPPED_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MappedId(u32);
pub struct MappedId(u64);
impl MappedId {
fn next() -> MappedId {
MappedId(MAPPED_ID_COUNTER.next())
}
pub fn get(self) -> u32 {
pub fn get(self) -> u64 {
self.0
}
}
@@ -139,6 +148,8 @@ impl Mapped {
animate_next_configure: false,
animate_serials: Vec::new(),
animation_snapshot: None,
transaction_for_next_configure: None,
pending_transactions: Vec::new(),
interactive_resize: None,
last_interactive_resize_start: Cell::new(None),
}
@@ -253,6 +264,40 @@ impl Mapped {
self.animation_snapshot = Some(self.render_snapshot(renderer));
}
pub fn take_pending_transaction(&mut self, commit_serial: Serial) -> Option<Transaction> {
let mut rv = None;
// Pending transactions are appended in order by serial, so we can loop from the start
// until we hit a serial that is too new.
while let Some((serial, _)) = self.pending_transactions.first() {
// In this loop, we will complete the transaction corresponding to the commit, as well
// as all transactions corresponding to previous serials. This can happen when we
// request resizes too quickly, and the surface only responds to the last one.
//
// Note that in this case, completing the previous transactions can result in an
// inconsistent visual state, if another window is waiting for this window to assume a
// specific size (in a previous transaction), which is now different (in this commit).
//
// However, there isn't really a good way to deal with that. We cannot cancel any
// transactions because we need to keep sending frame callbacks, and cancelling a
// transaction will make the corresponding frame callbacks get lost, and the window
// will hang.
//
// This is why resize throttling (implemented separately) is important: it prevents
// visually inconsistent states by way of never having more than one transaction in
// flight.
if commit_serial.is_no_older_than(serial) {
let (_, transaction) = self.pending_transactions.remove(0);
// Previous transaction is dropped here, signaling completion.
rv = Some(transaction);
} else {
break;
}
}
rv
}
pub fn last_interactive_resize_start(&self) -> &Cell<Option<(Duration, ResizeEdge)>> {
&self.last_interactive_resize_start
}
@@ -290,8 +335,8 @@ impl Mapped {
geo.size,
Rectangle::from_loc_and_size((0., 0.), geo.size),
GradientInterpolation::default(),
Color::from_array_premul(elem.color()),
Color::from_array_premul(elem.color()),
Color::from_color32f(elem.color()),
Color::from_color32f(elem.color()),
0.,
Rectangle::from_loc_and_size((0., 0.), geo.size),
0.,
@@ -440,7 +485,12 @@ impl LayoutElement for Mapped {
}
}
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>,
) {
let changed = self.toplevel().with_pending_state(|state| {
let changed = state.size != Some(size);
state.size = Some(size);
@@ -451,6 +501,15 @@ impl LayoutElement for Mapped {
if changed && animate {
self.animate_next_configure = true;
}
// Store the transaction regardless of whether the size changed. This is because with 3+
// windows in a column, the size may change among windows 1 and 2 and then right away among
// windows 2 and 3, and we want all windows 1, 2 and 3 to use the last transaction, rather
// than window 1 getting stuck with the previous transaction that is immediately released
// by 2.
if let Some(transaction) = transaction {
self.transaction_for_next_configure = Some(transaction);
}
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
@@ -512,7 +571,7 @@ impl LayoutElement for Mapped {
fn has_ssd(&self) -> bool {
let toplevel = self.toplevel();
let mode = toplevel.current_state().decoration_mode;
let mode = with_toplevel_role(self.toplevel(), |role| role.current.decoration_mode);
match mode {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) => true,
@@ -568,12 +627,71 @@ impl LayoutElement for Mapped {
});
}
fn configure_intent(&self) -> ConfigureIntent {
let _span =
trace_span!("configure_intent", surface = ?self.toplevel().wl_surface().id()).entered();
with_toplevel_role(self.toplevel(), |attributes| {
if let Some(server_pending) = &attributes.server_pending {
let current_server = attributes.current_server_state();
if server_pending != current_server {
// Something changed. Check if the only difference is the size, and if the
// current server size matches the current committed size.
let mut current_server_same_size = current_server.clone();
current_server_same_size.size = server_pending.size;
if current_server_same_size == *server_pending {
// Only the size changed. Check if the window committed our previous size
// request.
if attributes.current.size == current_server.size {
// The window had committed for our previous size change, so we can
// change the size again.
trace!(
"current size matches server size: {:?}",
attributes.current.size
);
ConfigureIntent::CanSend
} else {
// The window had not committed for our previous size change yet. Since
// nothing else changed, do not send the new size request yet. This
// throttling is done because some clients do not batch size requests,
// leading to bad behavior with very fast input devices (i.e. a 1000 Hz
// mouse). This throttling also helps interactive resize transactions
// preserve visual consistency.
trace!("throttling resize");
ConfigureIntent::Throttled
}
} else {
// Something else changed other than the size; send it.
trace!("something changed other than the size");
ConfigureIntent::ShouldSend
}
} else {
// Nothing changed since the last configure.
ConfigureIntent::NotNeeded
}
} else {
// Nothing changed since the last configure.
ConfigureIntent::NotNeeded
}
})
}
fn send_pending_configure(&mut self) {
let _span =
trace_span!("send_pending_configure", surface = ?self.toplevel().wl_surface().id())
.entered();
if let Some(serial) = self.toplevel().send_pending_configure() {
trace!(?serial, "sending configure");
if self.animate_next_configure {
self.animate_serials.push(serial);
}
if let Some(transaction) = self.transaction_for_next_configure.take() {
self.pending_transactions.push((serial, transaction));
}
self.interactive_resize = match self.interactive_resize.take() {
Some(InteractiveResize::WaitingForLastConfigure(data)) => {
Some(InteractiveResize::WaitingForLastCommit { data, serial })
@@ -590,13 +708,15 @@ impl LayoutElement for Mapped {
}
self.animate_next_configure = false;
self.transaction_for_next_configure = None;
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.current_state()
.states
.contains(xdg_toplevel::State::Fullscreen)
with_toplevel_role(self.toplevel(), |role| {
role.current
.states
.contains(xdg_toplevel::State::Fullscreen)
})
}
fn is_pending_fullscreen(&self) -> bool {
@@ -604,6 +724,10 @@ impl LayoutElement for Mapped {
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
fn requested_size(&self) -> Option<Size<i32, Logical>> {
self.toplevel().with_pending_state(|state| state.size)
}
fn refresh(&self) {
self.window.refresh();
}
+11 -14
View File
@@ -1,11 +1,9 @@
use niri_config::{BlockOutFrom, BorderRule, CornerRadius, Match, WindowRule};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::shell::xdg::{ToplevelSurface, XdgToplevelSurfaceRoleAttributes};
use crate::layout::workspace::ColumnWidth;
use crate::utils::with_toplevel_role;
pub mod mapped;
pub use mapped::Mapped;
@@ -72,6 +70,9 @@ pub struct ResolvedWindowRules {
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
/// Whether to enable VRR on this window's primary output if it is on-demand.
pub variable_refresh_rate: Option<bool>,
}
impl<'a> WindowRef<'a> {
@@ -132,6 +133,7 @@ impl ResolvedWindowRules {
geometry_corner_radius: None,
clip_to_geometry: None,
block_out_from: None,
variable_refresh_rate: None,
}
}
@@ -140,15 +142,7 @@ impl ResolvedWindowRules {
let mut resolved = ResolvedWindowRules::empty();
let toplevel = window.toplevel();
with_states(toplevel.wl_surface(), |states| {
let mut role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
with_toplevel_role(window.toplevel(), |role| {
// Ensure server_pending like in Smithay's with_pending_state().
if role.server_pending.is_none() {
role.server_pending = Some(role.current_server_state().clone());
@@ -165,7 +159,7 @@ impl ResolvedWindowRules {
}
}
window_matches(window, &role, m)
window_matches(window, role, m)
};
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
@@ -231,6 +225,9 @@ impl ResolvedWindowRules {
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
if let Some(x) = rule.variable_refresh_rate {
resolved.variable_refresh_rate = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
+3 -15
View File
@@ -1,20 +1,8 @@
### VSCode
There seems to be a bug in VSCode's Wayland backend until 1.86.0 which causes the window to not show up when using server-side decorations. So, to run VSCode:
1. Make sure VSCode is 1.86.0 or above, or that `prefer-no-csd` is **not set** in the niri config
2. Run `code --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations`
Also, if you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode. That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance. Apparently, VSCode currently unconditionally queries the X server for a keymap.
### Chromium
When creating new windows within Chromium (e.g. with <kbd>Ctrl</kbd><kbd>N</kbd>), there's a Chromium bug with sizing:
- With CSD (`prefer-no-csd` unset), the window will be a bit smaller than needed
- With SSD (`prefer-no-csd` set), the window buffer will be offset to the top-left
Both of these can be fixed by resizing the new Chromium window.
If you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode.
That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance.
Apparently, VSCode currently unconditionally queries the X server for a keymap.
### WezTerm
+66
View File
@@ -20,6 +20,10 @@ debug {
dbus-interfaces-in-non-session-instances
wait-for-frame-completion-before-queueing
emulate-zero-presentation-time
disable-resize-throttling
disable-transactions
keep-laptop-panel-on-when-lid-is-closed
disable-monitor-names
}
binds {
@@ -128,6 +132,68 @@ debug {
}
```
### `disable-resize-throttling`
<sup>Since: 0.1.9</sup>
Disable throttling resize events sent to windows.
By default, when resizing quickly (e.g. interactively), a window will only receive the next size once it has made a commit for the previously requested size.
This is required for resize transactions to work properly, and it also helps certain clients which don't batch incoming resizes from the compositor.
Disabling resize throttling will send resizes to windows as fast as possible, which is potentially very fast (for example, on a 1000 Hz mouse).
```kdl
debug {
disable-resize-throttling
}
```
### `disable-transactions`
<sup>Since: 0.1.9</sup>
Disable transactions (resize and close).
By default, windows which must resize together, do resize together.
For example, all windows in a column must resize at the same time to maintain the combined column height equal to the screen height, and to maintain the same window width.
Transactions make niri wait until all windows finish resizing before showing them all on screen in one, synchronized frame.
For them to work properly, resize throttling shouldn't be disabled (with the previous debug flag).
```kdl
debug {
disable-transactions
}
```
### `keep-laptop-panel-on-when-lid-is-closed`
<sup>Since: 0.1.10</sup>
By default, niri will disable the internal laptop monitor when the laptop lid is closed.
This flag turns off this behavior and will leave the internal laptop monitor on.
```kdl
debug {
keep-laptop-panel-on-when-lid-is-closed
}
```
### `disable-monitor-names`
<sup>Since: 0.1.10</sup>
Disables the make/model/serial monitor names, as if niri fails to read them from the EDID.
Use this flag to work around a crash present in 0.1.9 and 0.1.10 when connecting two monitors with matching make/model/serial.
```kdl
debug {
disable-monitor-names
}
```
### Key Bindings
These are not debug options, but rather key bindings.
+22 -1
View File
@@ -32,7 +32,9 @@ input {
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-factor 1.0
// scroll-method "two-finger"
// scroll-button 273
// tap-button-map "left-middle-right"
// click-method "clickfinger"
// left-handed
@@ -45,7 +47,9 @@ input {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-factor 1.0
// scroll-method "no-scroll"
// scroll-button 273
// left-handed
// middle-emulation
}
@@ -56,6 +60,18 @@ input {
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// middle-emulation
}
trackball {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// left-handed
// middle-emulation
}
@@ -134,13 +150,14 @@ A few settings are common between input devices:
- `off`: if set, no events will be sent from this device.
A few settings are common between `touchpad`, `mouse` and `trackpoint`:
A few settings are common between `touchpad`, `mouse`, `trackpoint`, and `trackball`:
- `natural-scroll`: if set, inverts the scrolling direction.
- `accel-speed`: pointer acceleration speed, valid values are from `-1.0` to `1.0` where the default is `0.0`.
- `accel-profile`: can be `adaptive` (the default) or `flat` (disables pointer acceleration).
- `scroll-method`: when to generate scroll events instead of pointer motion events, can be `no-scroll`, `two-finger`, `edge`, or `on-button-down`.
The default and supported methods vary depending on the device type.
- `scroll-button`: <sup>Since: 0.1.10</sup> the button code used for the `on-button-down` scroll method. You can find it in `libinput debug-events`.
- `middle-emulation`: emulate a middle mouse click by pressing left and right mouse buttons at once.
Settings specific to `touchpad`s:
@@ -152,6 +169,10 @@ Settings specific to `touchpad`s:
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
- `disabled-on-external-mouse`: do not send events while external pointer device is plugged in.
Settings specific to `touchpad` and `mouse`:
- `scroll-factor`: <sup>Since: 0.1.10</sup> scales the scrolling speed by this value.
Settings specific to `touchpad`, `mouse` and `tablet`:
- `left-handed`: if set, changes the device to left-handed mode.
+68
View File
@@ -8,6 +8,7 @@ Here are the contents of this section at a glance:
layout {
gaps 16
center-focused-column "never"
always-center-single-column
preset-column-widths {
proportion 0.33333
@@ -17,6 +18,12 @@ layout {
default-column-width { proportion 0.5; }
preset-window-heights {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
focus-ring {
// off
width 4
@@ -35,6 +42,12 @@ layout {
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
}
insert-hint {
// off
color "#ffc87f80"
// gradient from="#ffbb6680" to="#ffc88080" angle=45 relative-to="workspace-view"
}
struts {
// left 64
// right 64
@@ -75,6 +88,18 @@ layout {
}
```
### `always-center-single-column`
<sup>Since: 0.1.9</sup>
If set, niri will always center a single column on a workspace, regardless of the `center-focused-column` option.
```kdl
layout {
always-center-single-column
}
```
### `preset-column-widths`
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
@@ -134,6 +159,29 @@ layout {
>
> Either way, `default-column-width {}` is most useful for specific windows, in form of a [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules) with the same syntax.
### `preset-window-heights`
<sup>Since: 0.1.9</sup>
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
`proportion` sets the height as a fraction of the output height, taking gaps into account.
The default preset heights are <sup>1</sup>&frasl;<sub>3</sub>, <sup>1</sup>&frasl;<sub>2</sub> and <sup>2</sup>&frasl;<sub>3</sub> of the output.
`fixed` sets the height in logical pixels exactly.
```kdl
layout {
// Cycle between 1/3, 1/2, 2/3 of the output, and a fixed 720 logical pixels.
preset-window-heights {
proportion 0.33333
proportion 0.5
proportion 0.66667
fixed 720
}
}
```
### `focus-ring` and `border`
Focus ring and border are drawn around windows and indicate the active window.
@@ -261,6 +309,26 @@ layout {
}
```
### `insert-hint`
<sup>Since: 0.1.10</sup>
Settings for the window insert position hint during an interactive window move.
`off` disables the insert hint altogether.
`color` and `gradient` let you change the color of the hint and have the same syntax as colors and gradients in border and focus ring.
```kdl
layout {
insert-hint {
// off
color "#ffc87f80"
gradient from="#ffbb6680" to="#ffc88080" angle=45 relative-to="workspace-view"
}
}
```
### `struts`
Struts shrink the area occupied by windows, similarly to layer-shell panels.
+28
View File
@@ -20,6 +20,9 @@ environment {
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 48
hide-when-typing
hide-after-inactive-ms 1000
}
hotkey-overlay {
@@ -106,6 +109,31 @@ cursor {
}
```
#### `hide-when-typing`
<sup>Since: 0.1.10</sup>
If set, hides the cursor when pressing a key on the keyboard.
```kdl
cursor {
hide-when-typing
}
```
#### `hide-after-inactive-ms`
<sup>Since: 0.1.10</sup>
If set, the cursor will automatically hide once this number of milliseconds passes since the last cursor movement.
```kdl
cursor {
// Hide the cursor after one second of inactivity.
hide-after-inactive-ms 1000
}
```
### `hotkey-overlay`
Settings for the "Important Hotkeys" overlay.
+5 -2
View File
@@ -8,7 +8,7 @@ You can declare named workspaces at the top level of the config:
workspace "browser"
workspace "chat" {
open-on-output "DP-2"
open-on-output "Some Company CoolMonitor 1234"
}
```
@@ -24,7 +24,7 @@ workspace "chat" {
open-on-output "DP-2"
}
// Open Fractal on the "chat" workspace at niri startup.
// Open Fractal on the "chat" workspace, if it runs at niri startup.
window-rule {
match at-startup=true app-id=r#"^org\.gnome\.Fractal$"#
open-on-workspace "chat"
@@ -36,3 +36,6 @@ When editing the config while niri is running, newly declared named workspaces w
If you delete some named workspace from the config, the workspace will become normal (unnamed), and if there are no windows on it, it will be removed (as any other normal workspace).
There's no way to give a name to an already existing workspace, but you can simply move windows that you want to a new, empty named workspace.
<sup>Since: 0.1.9</sup> `open-on-output` can now use monitor manufacturer, model, and serial.
Before, it could only use the connector name.
+20 -3
View File
@@ -12,21 +12,29 @@ output "eDP-1" {
scale 2.0
transform "90"
position x=1280 y=0
variable-refresh-rate
variable-refresh-rate // on-demand=true
background-color "#003300"
}
output "HDMI-A-1" {
// ...settings for HDMI-A-1...
}
output "Some Company CoolMonitor 1234" {
// ...settings for CoolMonitor...
}
```
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`) which you can find by running `niri msg outputs`.
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`), or by monitor manufacturer, model, and serial, separated by a single space each.
You can find all of these by running `niri msg outputs`.
Usually, the built-in monitor in laptops will be called `eDP-1`.
Matching by output manufacturer and model is planned, but blocked on Smithay adopting libdisplay-info instead of edid-rs.
<sup>Since: 0.1.6</sup> The output name is case-insensitive.
<sup>Since: 0.1.9</sup> Outputs can be matched by manufacturer, model, and serial.
Before, they could be matched only by the connector name.
### `off`
This flag turns off that output entirely.
@@ -147,6 +155,15 @@ output "HDMI-A-1" {
}
```
<sup>Since: 0.1.9</sup> You can also set the `on-demand=true` property, which will only enable VRR when this output shows a window matching the `variable-refresh-rate` window rule.
This is helpful to avoid various issues with VRR, since it can be disabled most of the time, and only enabled for specific windows, like games or video players.
```kdl
output "HDMI-A-1" {
variable-refresh-rate on-demand=true
}
```
### `background-color`
<sup>Since: 0.1.8</sup>
+3 -2
View File
@@ -5,6 +5,7 @@ You can find documentation for various sections of the config on these wiki page
* [`input {}`](./Configuration:-Input.md)
* [`output "eDP-1" {}`](./Configuration:-Outputs.md)
* [`binds {}`](./Configuration:-Key-Bindings.md)
* [`switch-events {}`](./Configuration:-Switch-Events.md)
* [`layout {}`](./Configuration:-Layout.md)
* [top-level options](./Configuration:-Miscellaneous.md)
* [`window-rule {}`](./Configuration:-Window-Rules.md)
@@ -13,8 +14,8 @@ You can find documentation for various sections of the config on these wiki page
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`.
If that file is missing, niri will create it with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl).
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
+47
View File
@@ -0,0 +1,47 @@
### Overview
<sup>Since: 0.1.10</sup>
Switch event bindings are declared in the `switch-events {}` section of the config.
Here are all the events that you can bind at a glance:
```kdl
switch-events {
lid-close { spawn "notify-send" "The laptop lid is closed!"; }
lid-open { spawn "notify-send" "The laptop lid is open!"; }
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
}
```
The syntax is similar to key bindings.
Currently, only the `spawn` action are supported.
> [!NOTE]
> In contrast to key bindings, switch event bindings are *always* executed, even when the session is locked.
### `lid-close`, `lid-open`
These events correspond to closing and opening of the laptop lid.
Note that niri will already automatically turn the internal laptop monitor on and off in accordance with the laptop lid.
```kdl
switch-events {
lid-close { spawn "notify-send" "The laptop lid is closed!"; }
lid-open { spawn "notify-send" "The laptop lid is open!"; }
}
```
### `tablet-mode-on`, `tablet-mode-off`
These events trigger when a convertible laptop goes into or out of tablet mode.
In tablet mode, the keyboard and mouse are usually inaccessible, so you can use these events to activate the on-screen keyboard.
```kdl
switch-events {
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
}
```
+27 -1
View File
@@ -37,7 +37,7 @@ window-rule {
// Properties that apply once upon window opening.
default-column-width { proportion 0.75; }
open-on-output "eDP-1"
open-on-output "Some Company CoolMonitor 1234"
open-on-workspace "chat"
open-maximized true
open-fullscreen true
@@ -47,6 +47,7 @@ window-rule {
opacity 0.5
block-out-from "screencast"
// block-out-from "screen-capture"
variable-refresh-rate true
focus-ring {
// off
@@ -251,9 +252,14 @@ window-rule {
exclude app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
open-on-output "HDMI-A-1"
// Or:
// open-on-output "Some Company CoolMonitor 1234"
}
```
<sup>Since: 0.1.9</sup> `open-on-output` can now use monitor manufacturer, model, and serial.
Before, it could only use the connector name.
#### `open-on-workspace`
<sup>Since: 0.1.6</sup>
@@ -391,6 +397,26 @@ window-rule {
}
```
#### `variable-refresh-rate`
<sup>Since: 0.1.9</sup>
If set to true, whenever this window displays on an output with on-demand VRR, it will enable VRR on that output.
```kdl
// Configure some output with on-demand VRR.
output "HDMI-A-1" {
variable-refresh-rate on-demand=true
}
// Enable on-demand VRR when mpv displays on the output.
window-rule {
match app-id="^mpv$"
variable-refresh-rate true
}
```
#### `draw-border-with-background`
Override whether the border and the focus ring draw with a background.
+6
View File
@@ -22,3 +22,9 @@ And here are some more principles I try to follow throughout niri.
1. Eye-candy features should not cause unreasonable excessive rendering.
- For example, clip-to-geometry will prevent direct scanout in many cases (since the window surface is not completely visible). But in the cases where the surface or the subsurface *is* completely visible (fully within the clipped region), it will still allow for direct scanout.
- For example, animations *can* cause damage and even draw to an offscreen every frame, because they are expected to be short (and can be disabled). However, something like the rounded corners shader should not offscreen or cause excessive damage every frame, because it is long-running and constantly active.
1. Be mindful of invisible state.
This is niri state that is not immediately apparent from looking at the screen. This is not bad per se, but you should carefully consider how to reduce the surprise factor.
- For example, when a monitor disconnects, all its workspaces move to another connected monitor. In order to be able to restore these workspaces when the first monitor connects again, these workspaces keep the knowledge of which was their *original monitor*—this is an example of invisible state, since you can't tell it in any way by looking at the screen. This can have surprising consequences: imagine disconnecting a monitor at home, going to work, completely rearranging the windows there, then coming back home, and suddenly some random workspaces end up on your home monitor. In order to reduce this surprise factor, whenever a new window appears on a workspace, that workspace resets its *original monitor* to its current monitor. This way, the workspaces you actively worked on remain where they were.
- For example, niri preserves the view position whenever a window appears, or whenever a window goes full-screen, to restore it afterward. This way, dealing with temporary things like dialogs opening and closing, or toggling full-screen, becomes less annoying, since it doesn't mess up the view position. This is also invisible state, as you cannot tell by looking at the screen where closing a window will restore the view position. If taken to the extreme (previous view position saved forever for every open window), this can be surprising, as closing long-running windows would result in the view shifting around pretty much randomly. To reduce this surprise factor, niri remembers only one last view position per workspace, and forgets this stored view position upon window focus change.
+2
View File
@@ -72,3 +72,5 @@ pub fn some_function() {
// Code of the function.
}
```
You can also enable Rust memory allocation profiling with `--features=profile-with-tracy-allocations`.
+18 -1
View File
@@ -28,7 +28,7 @@ By default, Waybar is on the `bottom` layer, which is behind windows, so Waybar
Put this window rule in your config:
```
```kdl
window-rule {
geometry-corner-radius 12
clip-to-geometry true
@@ -36,3 +36,20 @@ window-rule {
```
For more information, check [this wiki section](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#geometry-corner-radius).
### How to hide the "Important Hotkeys" pop-up at the start?
Put this into your config:
```kdl
hotkey-overlay {
skip-at-startup
}
```
### How to run X11 apps like Steam or Discord?
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
Keep in mind that you can run many Electron apps such as VSCode natively on Wayland by passing the right flags, e.g. `code --ozone-platform-hint=auto`
+8
View File
@@ -4,6 +4,14 @@ There are several gestures in niri.
### Mouse
#### Interactive Move
<sup>Since: 0.1.10</sup>
You can move windows by holding <kbd>Mod</kbd> and the left mouse button.
You can customize the look of the window insertion preview in the `insert-hint` [layout config](./Configuration:-Layout.md) section.
#### Interactive Resize
<sup>Since: 0.1.6</sup>
+19 -18
View File
@@ -8,8 +8,9 @@ After installing, start niri from your display manager like GDM.
Press <kbd>Super</kbd><kbd>T</kbd> to run a terminal ([Alacritty]) and <kbd>Super</kbd><kbd>D</kbd> to run an application launcher ([fuzzel]).
To exit niri, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
If you're not using a display manager, you should run `niri-session` (systemd) or `niri --session` (not systemd) from a TTY.
The `--session` flag will make niri import its environment variables globally into systemd and D-Bus, and start its D-Bus services.
If you're not using a display manager, you should run `niri-session` (systemd/dinit) or `niri --session` (others) from a TTY.
The `--session` flag will make niri import its environment variables globally into the system manager and D-Bus, and start its D-Bus services.
The `niri-session` script will additionally start niri as a systemd/dinit service, which starts up a graphical session target required by some services like portals.
You can also run `niri` inside an existing desktop session.
Then it will open as a window, where you can give it a try.
@@ -22,21 +23,13 @@ Finally, the [Xwayland](./Xwayland.md) page explains how to run X11 applications
### NVIDIA
NVIDIA GPUs tend to have problems running niri (for example, the screen remains black upon starting from a TTY).
NVIDIA GPUs can have problems running niri (for example, the screen remains black upon starting from a TTY).
Sometimes, the problems can be fixed.
You can try the following:
1. Update NVIDIA drivers. You need a GPU and drivers recent enough to support GBM.
2. Make sure kernel modesetting is enabled. This usually involves adding `nvidia-drm.modeset=1` to the kernel command line. Find and follow a guide for your distribution. Guides from other Wayland compositors can help.
If niri runs but the screen flickers, try adding this into your niri config:
```
debug {
wait-for-frame-completion-before-queueing
}
```
### Asahi, ARM, and other kmsro devices
On some of these systems, niri fails to correctly detect the primary render device.
@@ -69,6 +62,7 @@ If you still get a black screen, try using each of the `card` devices.
### Nix/NixOS
There's a common problem of mesa drivers going out of sync with niri, so make sure your system mesa version matches the niri mesa version.
When this happens, you usually see a black screen when trying to start niri from a TTY.
Also, on Intel graphics, you may need a workaround described [here](https://nixos.wiki/wiki/Intel_Graphics).
@@ -88,7 +82,7 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
| <kbd>Super</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
@@ -112,6 +106,8 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
@@ -119,6 +115,7 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>R</kbd> | Reset window height back to automatic |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
@@ -132,13 +129,13 @@ First, install the dependencies for your distribution.
- Ubuntu 23.10:
```sh
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
```
- Fedora:
```sh
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libdisplay-info-devel
```
Next, get latest stable Rust: https://rustup.rs/
@@ -175,8 +172,10 @@ To do that, put files into the correct directories according to this table.
| `resources/niri-session` | `/usr/bin/` |
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
| `resources/niri.service` | `/usr/lib/systemd/user/` |
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
| `resources/niri.service` (systemd) | `/usr/lib/systemd/user/` |
| `resources/niri-shutdown.target` (systemd) | `/usr/lib/systemd/user/` |
| `resources/dinit/niri` (dinit) | `/usr/lib/dinit.d/user/` |
| `resources/dinit/niri-shutdown` (dinit) | `/usr/lib/dinit.d/user/` |
Doing this will make niri appear in GDM and other display managers.
@@ -192,8 +191,10 @@ These may vary depending on your distribution.
| `resources/niri-session` | `/usr/local/bin/` |
| `resources/niri.desktop` | `/usr/local/share/wayland-sessions/` |
| `resources/niri-portals.conf` | `/usr/local/share/xdg-desktop-portal/` |
| `resources/niri.service` | `/etc/systemd/user/` |
| `resources/niri-shutdown.target` | `/etc/systemd/user/` |
| `resources/niri.service` (systemd) | `/etc/systemd/user/` |
| `resources/niri-shutdown.target` (systemd) | `/etc/systemd/user/` |
| `resources/dinit/niri` (dinit) | `/etc/dinit.d/user/` |
| `resources/dinit/niri-shutdown` (dinit) | `/etc/dinit.d/user/` |
[Alacritty]: https://github.com/alacritty/alacritty
[fuzzel]: https://codeberg.org/dnkl/fuzzel
+51 -5
View File
@@ -4,13 +4,61 @@ Check `niri msg --help` for available commands.
The `--json` flag prints the response in JSON, rather than formatted.
For example, `niri msg --json outputs`.
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
The communication over the IPC socket happens in JSON.
> [!TIP]
> If you're getting parsing errors from `niri msg` after upgrading niri, make sure that you've restarted niri itself.
> You might be trying to run a newer `niri msg` against an older `niri` compositor.
### Event Stream
<sup>Since: 0.1.9</sup>
While most niri IPC requests return a single response, the event stream request will make niri continuously stream events into the IPC connection until it is closed.
This is useful for implementing various bars and indicators that update as soon as something happens, without continuous polling.
The event stream IPC is designed to give you the complete current state up-front, then follow up with updates to that state.
This way, your state can never "desync" from niri, and you don't need to make any other IPC information requests.
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 workspaces-changed event arrives before the corresponding window-changed event.
To get a taste of the events, run `niri msg event-stream`.
Though, this is more of a debug function than anything.
You can get raw events from `niri msg --json event-stream`, or by connecting to the niri socket and requesting an event stream manually.
You can find the full list of events along with documentation [here](https://yalter.github.io/niri/niri_ipc/enum.Event.html).
### Programmatic Access
`niri msg --json` is a thin wrapper over writing and reading to a socket.
When implementing more complex scripts and modules, you're encouraged to access the socket directly.
Connect to the UNIX domain socket located at `$NIRI_SOCKET` in the filesystem.
Write your request encoded in JSON on a single line, followed by a newline character, or by flushing and shutting down the write end of the connection.
Read the reply as JSON, also on a single line.
You can use `socat` to test communicating with niri directly:
```sh
$ socat STDIO "$NIRI_SOCKET"
"FocusedWindow"
{"Ok":{"FocusedWindow":{"id":12,"title":"t socat STDIO /run/u ~","app_id":"Alacritty","workspace_id":6,"is_focused":true}}}
```
The reply is an `Ok` or an `Err` wrapping the same JSON object as you get from `niri msg --json`.
For more complex requests, you can use `socat` to find how `niri msg` formats them:
```sh
$ socat STDIO UNIX-LISTEN:temp.sock
# then, in a different terminal:
$ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2
# then, look in the socat terminal:
{"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}
```
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://yalter.github.io/niri/niri_ipc/).
### Backwards Compatibility
The JSON output *should* remain stable, as in:
@@ -20,8 +68,6 @@ The JSON output *should* remain stable, as in:
However, new fields and enum variants will be added, so you should handle unknown fields or variants gracefully where reasonable.
I am not 100% committing to the stability yet because there aren't many users, and there might be something basic I had missed in the JSON output design.
The formatted/human-readable output (i.e. without `--json` flag) is **not** considered stable.
Please prefer the JSON output for scripts, since I reserve the right to make any changes to the human-readable output.
+7
View File
@@ -33,3 +33,10 @@ systemctl --user edit --full plasma-polkit-agent.service
```
Then add `After=graphical-session.target`.
### Xwayland
To run X11 apps like Steam or Discord, you can use [xwayland-satellite].
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
[xwayland-satellite]: https://github.com/Supreeeme/xwayland-satellite
+14
View File
@@ -15,6 +15,20 @@ env DISPLAY=:0 flatpak run com.valvesoftware.Steam
They will appear as normal windows.
You can also set `DISPLAY` by default for all apps by adding it to the `environment` section of the niri config:
```kdl
environment {
DISPLAY ":0"
}
```
> [!NOTE]
> If the `:0` DISPLAY is already taken (for example, by some other Xwayland server like `xwayland-run`), `xwayland-satellite` will try the next DISPLAY numbers in order: `:1`, `:2`, etc. and tell you which one it used in its output.
> Then, you will need to use that DISPLAY number for the `env` command or for the niri `environment` block.
>
> You can also force a specific DISPLAY number like so: `xwayland-satellite :12` will start on `DISPLAY=:12`.
## Directly running Xwayland in rootful mode
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
+2 -1
View File
@@ -3,7 +3,7 @@
* [Example systemd Setup](./Example-systemd-Setup.md)
* [Important Software](./Important-Software.md)
* [LayerShell Components](./Layer%E2%80%90Shell-Components.md)
* [`niri msg`](./IPC.md)
* [IPC, `niri msg`](./IPC.md)
* [VSCode, Chromium, WezTerm](./Application-Issues.md)
* [Xwayland](./Xwayland.md)
* [Gestures](./Gestures.md)
@@ -14,6 +14,7 @@
* [Input](./Configuration:-Input.md)
* [Outputs](./Configuration:-Outputs.md)
* [Key Bindings](./Configuration:-Key-Bindings.md)
* [Switch Events](./Configuration:-Switch-Events.md)
* [Layout](./Configuration:-Layout.md)
* [Named Workspaces](./Configuration:-Named-Workspaces.md)
* [Miscellaneous](./Configuration:-Miscellaneous.md)