Compare commits

..

113 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
54 changed files with 5111 additions and 1944 deletions
+1 -1
View File
@@ -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:
Generated
+614 -398
View File
File diff suppressed because it is too large Load Diff
+27 -25
View File
@@ -2,7 +2,7 @@
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.9"
version = "0.1.10-1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -11,15 +11,15 @@ repository = "https://github.com/YaLTeR/niri"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.88"
anyhow = "1.0.93"
bitflags = "2.6.0"
clap = { version = "4.5.17", features = ["derive"] }
clap = { version = "4.5.20", features = ["derive"] }
k9 = "0.12.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
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.3", default-features = false }
tracy-client = { version = "0.17.4", default-features = false }
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
@@ -50,38 +50,38 @@ async-channel = "2.3.1"
async-io = { version = "1.13.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.18.0", features = ["derive"] }
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.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.0"
fastrand = "2.2.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.29.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.158"
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.9", path = "niri-config" }
niri-ipc = { version = "0.1.9", 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.1", features = ["v1_44"] }
pangocairo = "0.20.1"
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 }
url = { version = "2.5.3", optional = true }
wayland-backend = "0.3.7"
wayland-scanner = "0.31.5"
xcursor = "0.3.8"
@@ -109,7 +109,7 @@ features = [
approx = "0.5.1"
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]
@@ -124,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 = []
@@ -137,7 +139,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.9"
version = "0.1.10.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
Generated
+18 -92
View File
@@ -1,65 +1,5 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1724533099,
"narHash": "sha256-ZIDtvVQHoCkNoBlLUB3wmqbqCb0Es3DfdUGeDI/58aY=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7543c8d76f91b8844e0f3b3cc347a72d8fb4b49e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1722493751,
"narHash": "sha256-l7/yMehbrL5d4AI8E2hKtNlT50BlUAau4EKTgPg9KcY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "60ab4a085ef6ee40f2ef7921ca4061084dd8cf26",
"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": 1724395761,
"narHash": "sha256-zRkDV/nbrnp3Y8oCADf5ETl1sDrdmAW6/bBVJ8EbIdQ=",
"lastModified": 1726365531,
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ae815cee91b417be55d43781eb4b73ae1ecc396c",
"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": 1722449213,
"narHash": "sha256-1na4m2PNH99syz2g/WQ+Hr3RfY7k4H8NBnmkr5dFDXw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c8e41d95061543715b30880932ec3dc24c42d7ae",
"lastModified": 1727663505,
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
+224 -77
View File
@@ -4,101 +4,248 @@
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.mkLib pkgs).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
libdisplay-info
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
cairo
glib
pixman
];
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";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath craneArgs.runtimeDependencies; # Needed for tests to find libxkbcommon
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 = craneLib.devShell {
inputsFrom = [niri];
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"
];
}
))
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (craneArgs.runtimeDependencies ++ craneArgs.nativeBuildInputs ++ craneArgs.buildInputs);
inherit (niri) LIBCLANG_PATH;
};
}
);
nativeBuildInputs = [
pkgs.clang
pkgs.pkg-config
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
];
buildInputs = niri.buildInputs ++ [
pkgs.libadwaita # For `niri-visual-tests`
];
env = {
inherit (niri) LIBCLANG_PATH;
# WARN: Do not overwrite this variable in your shell!
# It is required for `dlopen()` to work on some libraries; see the comment
# in the package expression
#
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
};
};
}
);
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
packages = forAllSystems (
system:
let
niri = nixpkgsFor.${system}.callPackage niri-package { };
in
{
inherit niri;
# NOTE: This is for development purposes only
#
# It is primarily to help with quickly iterating on
# changes made to the above expression - though it is
# also not stripped in order to better debug niri itself
niri-debug = niri.overrideAttrs (
newAttrs: oldAttrs: {
pname = oldAttrs.pname + "-debug";
cargoBuildType = "debug";
cargoCheckType = newAttrs.cargoBuildType;
dontStrip = true;
}
);
default = niri;
}
);
overlays.default = final: _: {
niri = final.callPackage niri-package { };
};
};
}
+3 -3
View File
@@ -12,8 +12,8 @@ bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.9", 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
@@ -21,4 +21,4 @@ tracy-client.workspace = true
[dev-dependencies]
k9.workspace = true
miette = { version = "5.10.0", features = ["fancy"] }
pretty_assertions = "1.4.0"
pretty_assertions = "1.4.1"
+188 -6
View File
@@ -54,6 +54,8 @@ pub struct Config {
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
pub switch_events: SwitchBinds,
#[knuffel(child, default)]
pub debug: DebugConfig,
#[knuffel(children(name = "workspace"))]
pub workspaces: Vec<Workspace>,
@@ -70,6 +72,8 @@ pub struct Input {
#[knuffel(child, default)]
pub trackpoint: Trackpoint,
#[knuffel(child, default)]
pub trackball: Trackball,
#[knuffel(child, default)]
pub tablet: Tablet,
#[knuffel(child, default)]
pub touch: Touch,
@@ -174,6 +178,8 @@ pub struct Touchpad {
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child, unwrap(argument, str))]
pub tap_button_map: Option<TapButtonMap>,
#[knuffel(child)]
@@ -182,6 +188,8 @@ pub struct Touchpad {
pub disabled_on_external_mouse: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child, unwrap(argument))]
pub scroll_factor: Option<FloatOrInt<0, 100>>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -196,10 +204,14 @@ pub struct Mouse {
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child, unwrap(argument))]
pub scroll_factor: Option<FloatOrInt<0, 100>>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -214,6 +226,28 @@ pub struct Trackpoint {
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub middle_emulation: bool,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Trackball {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
}
@@ -391,6 +425,8 @@ pub struct Layout {
pub focus_ring: FocusRing,
#[knuffel(child, default)]
pub border: Border,
#[knuffel(child, default)]
pub insert_hint: InsertHint,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetSize>,
#[knuffel(child)]
@@ -412,6 +448,7 @@ impl Default for Layout {
Self {
focus_ring: Default::default(),
border: Default::default(),
insert_hint: Default::default(),
preset_column_widths: Default::default(),
default_column_width: Default::default(),
center_focused_column: Default::default(),
@@ -558,6 +595,26 @@ impl From<FocusRing> for Border {
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct InsertHint {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, default = Self::default().color)]
pub color: Color,
#[knuffel(child)]
pub gradient: Option<Gradient>,
}
impl Default for InsertHint {
fn default() -> Self {
Self {
off: false,
color: Color::from_rgba8_unpremul(127, 200, 255, 128),
gradient: None,
}
}
}
/// RGB color in [0, 1] with unpremultiplied alpha.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Color {
@@ -615,6 +672,10 @@ pub struct Cursor {
pub xcursor_theme: String,
#[knuffel(child, unwrap(argument), default = 24)]
pub xcursor_size: u8,
#[knuffel(child)]
pub hide_when_typing: bool,
#[knuffel(child, unwrap(argument))]
pub hide_after_inactive_ms: Option<u32>,
}
impl Default for Cursor {
@@ -622,6 +683,8 @@ impl Default for Cursor {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
hide_when_typing: false,
hide_after_inactive_ms: None,
}
}
}
@@ -1054,6 +1117,24 @@ bitflags! {
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct SwitchBinds {
#[knuffel(child)]
pub lid_open: Option<SwitchAction>,
#[knuffel(child)]
pub lid_close: Option<SwitchAction>,
#[knuffel(child)]
pub tablet_mode_on: Option<SwitchAction>,
#[knuffel(child)]
pub tablet_mode_off: Option<SwitchAction>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct SwitchAction {
#[knuffel(child, unwrap(arguments))]
pub spawn: Vec<String>,
}
// Remember to add new actions to the CLI enum too.
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum Action {
@@ -1062,6 +1143,7 @@ pub enum Action {
ChangeVt(i32),
Suspend,
PowerOffMonitors,
PowerOnMonitors,
ToggleDebugTint,
DebugToggleOpaqueRegions,
DebugToggleDamage,
@@ -1115,7 +1197,11 @@ pub enum Action {
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeOrExpelWindowLeft,
#[knuffel(skip)]
ConsumeOrExpelWindowLeftById(u64),
ConsumeOrExpelWindowRight,
#[knuffel(skip)]
ConsumeOrExpelWindowRightById(u64),
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
@@ -1176,6 +1262,7 @@ impl From<niri_ipc::Action> for Action {
match value {
niri_ipc::Action::Quit { skip_confirmation } => Self::Quit(skip_confirmation),
niri_ipc::Action::PowerOffMonitors {} => Self::PowerOffMonitors,
niri_ipc::Action::PowerOnMonitors {} => Self::PowerOnMonitors,
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms),
niri_ipc::Action::Screenshot {} => Self::Screenshot,
@@ -1221,8 +1308,18 @@ impl From<niri_ipc::Action> for Action {
Self::MoveWindowDownOrToWorkspaceDown
}
niri_ipc::Action::MoveWindowUpOrToWorkspaceUp {} => Self::MoveWindowUpOrToWorkspaceUp,
niri_ipc::Action::ConsumeOrExpelWindowLeft {} => Self::ConsumeOrExpelWindowLeft,
niri_ipc::Action::ConsumeOrExpelWindowRight {} => Self::ConsumeOrExpelWindowRight,
niri_ipc::Action::ConsumeOrExpelWindowLeft { id: None } => {
Self::ConsumeOrExpelWindowLeft
}
niri_ipc::Action::ConsumeOrExpelWindowLeft { id: Some(id) } => {
Self::ConsumeOrExpelWindowLeftById(id)
}
niri_ipc::Action::ConsumeOrExpelWindowRight { id: None } => {
Self::ConsumeOrExpelWindowRight
}
niri_ipc::Action::ConsumeOrExpelWindowRight { id: Some(id) } => {
Self::ConsumeOrExpelWindowRightById(id)
}
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
@@ -1436,6 +1533,10 @@ pub struct DebugConfig {
pub disable_resize_throttling: bool,
#[knuffel(child)]
pub disable_transactions: bool,
#[knuffel(child)]
pub keep_laptop_panel_on_when_lid_is_closed: bool,
#[knuffel(child)]
pub disable_monitor_names: bool,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
@@ -1817,13 +1918,17 @@ impl OutputName {
if self.make.is_none() && self.model.is_none() && self.serial.is_none() {
self.connector.to_string()
} else {
let make = self.make.as_deref().unwrap_or("Unknown");
let model = self.model.as_deref().unwrap_or("Unknown");
let serial = self.serial.as_deref().unwrap_or("Unknown");
format!("{make} {model} {serial}")
self.format_make_model_serial()
}
}
pub fn format_make_model_serial(&self) -> String {
let make = self.make.as_deref().unwrap_or("Unknown");
let model = self.model.as_deref().unwrap_or("Unknown");
let serial = self.serial.as_deref().unwrap_or("Unknown");
format!("{make} {model} {serial}")
}
pub fn matches(&self, target: &str) -> bool {
// Match by connector.
if target.eq_ignore_ascii_case(&self.connector) {
@@ -2862,8 +2967,10 @@ mod tests {
accel-speed 0.2
accel-profile "flat"
scroll-method "two-finger"
scroll-button 272
tap-button-map "left-middle-right"
disabled-on-external-mouse
scroll-factor 0.9
}
mouse {
@@ -2871,7 +2978,9 @@ mod tests {
accel-speed 0.4
accel-profile "flat"
scroll-method "no-scroll"
scroll-button 273
middle-emulation
scroll-factor 0.2
}
trackpoint {
@@ -2880,6 +2989,18 @@ mod tests {
accel-speed 0.0
accel-profile "flat"
scroll-method "on-button-down"
scroll-button 274
}
trackball {
off
natural-scroll
accel-speed 0.0
accel-profile "flat"
scroll-method "edge"
scroll-button 275
left-handed
middle-emulation
}
tablet {
@@ -2944,6 +3065,11 @@ mod tests {
}
center-focused-column "on-overflow"
insert-hint {
color "rgb(255, 200, 127)"
gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view"
}
}
spawn-at-startup "alacritty" "-e" "fish"
@@ -2953,6 +3079,8 @@ mod tests {
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 16
hide-when-typing
hide-after-inactive-ms 3000
}
screenshot-path "~/Screenshots/screenshot.png"
@@ -3013,6 +3141,11 @@ mod tests {
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
}
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"; }
}
debug {
render-drm-device "/dev/dri/renderD129"
}
@@ -3045,10 +3178,12 @@ mod tests {
accel_speed: 0.2,
accel_profile: Some(AccelProfile::Flat),
scroll_method: Some(ScrollMethod::TwoFinger),
scroll_button: Some(272),
tap_button_map: Some(TapButtonMap::LeftMiddleRight),
left_handed: false,
disabled_on_external_mouse: true,
middle_emulation: false,
scroll_factor: Some(FloatOrInt(0.9)),
},
mouse: Mouse {
off: false,
@@ -3056,8 +3191,10 @@ mod tests {
accel_speed: 0.4,
accel_profile: Some(AccelProfile::Flat),
scroll_method: Some(ScrollMethod::NoScroll),
scroll_button: Some(273),
left_handed: false,
middle_emulation: true,
scroll_factor: Some(FloatOrInt(0.2)),
},
trackpoint: Trackpoint {
off: true,
@@ -3065,8 +3202,19 @@ mod tests {
accel_speed: 0.0,
accel_profile: Some(AccelProfile::Flat),
scroll_method: Some(ScrollMethod::OnButtonDown),
scroll_button: Some(274),
middle_emulation: false,
},
trackball: Trackball {
off: true,
natural_scroll: true,
accel_speed: 0.0,
accel_profile: Some(AccelProfile::Flat),
scroll_method: Some(ScrollMethod::Edge),
scroll_button: Some(275),
left_handed: true,
middle_emulation: true,
},
tablet: Tablet {
off: false,
map_to_output: Some("eDP-1".to_owned()),
@@ -3122,6 +3270,20 @@ mod tests {
active_gradient: None,
inactive_gradient: None,
},
insert_hint: InsertHint {
off: false,
color: Color::from_rgba8_unpremul(255, 200, 127, 255),
gradient: Some(Gradient {
from: Color::from_rgba8_unpremul(10, 20, 30, 255),
to: Color::from_rgba8_unpremul(0, 128, 255, 255),
angle: 180,
relative_to: GradientRelativeTo::WorkspaceView,
in_: GradientInterpolation {
color_space: GradientColorSpace::Srgb,
hue_interpolation: HueInterpolation::Shorter,
},
}),
},
preset_column_widths: vec![
PresetSize::Proportion(0.25),
PresetSize::Proportion(0.5),
@@ -3154,6 +3316,8 @@ mod tests {
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
hide_when_typing: true,
hide_after_inactive_ms: Some(3000),
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
hotkey_overlay: HotkeyOverlay {
@@ -3345,6 +3509,24 @@ mod tests {
allow_when_locked: false,
},
]),
switch_events: SwitchBinds {
lid_open: None,
lid_close: None,
tablet_mode_on: Some(SwitchAction {
spawn: vec![
"bash".to_owned(),
"-c".to_owned(),
"gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true".to_owned(),
],
}),
tablet_mode_off: Some(SwitchAction {
spawn: vec![
"bash".to_owned(),
"-c".to_owned(),
"gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false".to_owned(),
],
}),
},
debug: DebugConfig {
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
..Default::default()
+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"
```
+40 -4
View File
@@ -19,6 +19,20 @@
//!
//! 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;
@@ -132,6 +146,8 @@ pub enum Action {
},
/// Power off all monitors via DPMS.
PowerOffMonitors {},
/// Power on all monitors via DPMS.
PowerOnMonitors {},
/// Spawn a command.
Spawn {
/// Command to spawn.
@@ -240,10 +256,30 @@ pub enum Action {
MoveWindowDownOrToWorkspaceDown {},
/// Move the focused window up in a column or to the workspace above.
MoveWindowUpOrToWorkspaceUp {},
/// Consume or expel the focused window left.
ConsumeOrExpelWindowLeft {},
/// Consume or expel the focused window right.
ConsumeOrExpelWindowRight {},
/// Consume 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 {},
/// Expel the focused window from the column.
+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.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.9", path = ".." }
niri-config = { version = "0.1.9", 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
+1 -6
View File
@@ -198,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) {
@@ -233,7 +229,6 @@ impl TestCase for Layout {
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
+23 -1
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
@@ -202,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.
@@ -250,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
@@ -422,7 +441,9 @@ 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.
@@ -465,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
+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
+107 -48
View File
@@ -61,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];
@@ -636,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;
@@ -663,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) {
@@ -749,7 +763,12 @@ impl Tty {
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
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())
@@ -767,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()
@@ -779,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:?}");
}
@@ -1019,6 +1029,14 @@ 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 {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
@@ -1050,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(())
@@ -1557,8 +1574,12 @@ impl Tty {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name.clone());
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());
@@ -1659,10 +1680,9 @@ 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:?}");
}
}
}
@@ -1705,6 +1725,24 @@ 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![];
@@ -1717,7 +1755,7 @@ 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;
}
@@ -1813,12 +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 connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let output_name = make_output_name(
&device.drm,
connector.handle(),
connector_name,
self.config.borrow().debug.disable_monitor_names,
);
let config = self
.config
.borrow()
@@ -1827,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));
}
}
}
@@ -1837,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:?}");
}
@@ -1872,12 +1923,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 connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
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);
}
@@ -2154,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;
@@ -2486,7 +2535,17 @@ 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,
};
}
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
+2 -1
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,7 +64,7 @@ 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-"));
let is_laptop_panel = is_laptop_panel(c);
let display_name = make_display_name(output, is_laptop_panel);
let mut properties = HashMap::new();
+41 -2
View File
@@ -1,7 +1,7 @@
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;
@@ -297,13 +297,52 @@ impl CompositorHandler for State {
&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(&root_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;
+99 -21
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,7 +67,7 @@ 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,
};
@@ -72,12 +75,14 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
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_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;
@@ -135,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);
@@ -225,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();
}
@@ -388,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;
}
@@ -403,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);
}
}
@@ -551,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);
}
}
}
+150 -55
View File
@@ -23,12 +23,13 @@ use smithay::wayland::compositor::{
};
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::{
@@ -36,8 +37,11 @@ use smithay::{
};
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::transaction::Transaction;
@@ -65,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(
@@ -76,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;
};
@@ -128,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(
@@ -261,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();
}
}
@@ -288,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);
}
}
@@ -332,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
@@ -416,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
@@ -552,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();
}
}
@@ -565,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();
}
}
@@ -620,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");
@@ -706,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;
@@ -759,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();
@@ -788,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();
+375 -71
View File
@@ -6,14 +6,15 @@ use std::time::Duration;
use calloop::timer::{TimeoutAction, Timer};
use input::event::gesture::GestureEventCoordinates as _;
use niri_config::{Action, Bind, Binds, Key, Modifiers, Trigger};
use niri_config::{Action, Bind, Binds, Key, Modifiers, SwitchBinds, Trigger};
use niri_ipc::LayoutSwitchTarget;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
InputBackend, InputEvent, KeyState, KeyboardKeyEvent, MouseButton, PointerAxisEvent,
PointerButtonEvent, PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode, MouseButton, PointerAxisEvent,
PointerButtonEvent, PointerMotionEvent, ProximityState, Switch, SwitchState, SwitchToggleEvent,
TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent,
TabletToolTipState, TouchEvent,
};
use smithay::backend::libinput::LibinputInputBackend;
use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState};
@@ -23,11 +24,16 @@ use smithay::input::pointer::{
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, RelativeMotionEvent,
};
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
use smithay::utils::{Logical, Point, Rectangle, SERIAL_COUNTER};
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER};
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use touch_move_grab::TouchMoveGrab;
use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::niri::State;
@@ -35,10 +41,13 @@ use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
use crate::utils::{center, get_monotonic_time, ResizeEdge};
pub mod move_grab;
pub mod resize_grab;
pub mod scroll_tracker;
pub mod spatial_movement_grab;
pub mod swipe_tracker;
pub mod touch_move_grab;
pub mod touch_resize_grab;
pub const DOUBLE_CLICK_TIME: Duration = Duration::from_millis(400);
@@ -53,6 +62,20 @@ pub struct TabletData {
pub aspect_ratio: f64,
}
pub enum PointerOrTouchStartData<D: SeatHandler> {
Pointer(PointerGrabStartData<D>),
Touch(TouchGrabStartData<D>),
}
impl<D: SeatHandler> PointerOrTouchStartData<D> {
pub fn location(&self) -> Point<f64, Logical> {
match self {
PointerOrTouchStartData::Pointer(x) => x.location,
PointerOrTouchStartData::Touch(x) => x.location,
}
}
}
impl State {
pub fn process_input_event<I: InputBackend + 'static>(&mut self, event: InputEvent<I>)
where
@@ -87,6 +110,10 @@ impl State {
}
}
if should_reset_pointer_inactivity_timer(&event) {
self.niri.reset_pointer_inactivity_timer();
}
let hide_hotkey_overlay =
self.niri.hotkey_overlay.is_open() && should_hide_hotkey_overlay(&event);
@@ -123,7 +150,7 @@ impl State {
TouchUp { event } => self.on_touch_up::<I>(event),
TouchCancel { event } => self.on_touch_cancel::<I>(event),
TouchFrame { event } => self.on_touch_frame::<I>(event),
SwitchToggle { .. } => (),
SwitchToggle { event } => self.on_switch_toggle::<I>(event),
Special(_) => (),
}
@@ -239,26 +266,31 @@ impl State {
where
I::Device: 'static,
{
let (target_geo, keep_ratio, px) = if let Some(output) = self.niri.output_for_tablet() {
(
self.niri.global_space.output_geometry(output).unwrap(),
true,
1. / output.current_scale().fractional_scale(),
)
} else {
let geo = self.global_bounding_rectangle()?;
let (target_geo, keep_ratio, px, transform) =
if let Some(output) = self.niri.output_for_tablet() {
(
self.niri.global_space.output_geometry(output).unwrap(),
true,
1. / output.current_scale().fractional_scale(),
output.current_transform(),
)
} else {
let geo = self.global_bounding_rectangle()?;
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
// corresponding to the position on the right when clamping.
let output = self.niri.global_space.outputs().next().unwrap();
let scale = output.current_scale().fractional_scale();
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
// corresponding to the position on the right when clamping.
let output = self.niri.global_space.outputs().next().unwrap();
let scale = output.current_scale().fractional_scale();
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
(geo, false, 1. / scale)
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
(geo, false, 1. / scale, Transform::Normal)
};
let mut pos = {
let size = transform.invert().transform_size(target_geo.size);
transform.transform_point_in(event.position_transformed(size), &size.to_f64())
};
let mut pos = event.position_transformed(target_geo.size);
if keep_ratio {
pos.x /= target_geo.size.w as f64;
pos.y /= target_geo.size.h as f64;
@@ -267,7 +299,8 @@ impl State {
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if let Some(data) = self.niri.tablets.get(device) {
// This code does the same thing as mutter with "keep aspect ratio" enabled.
let output_aspect_ratio = target_geo.size.w as f64 / target_geo.size.h as f64;
let size = transform.invert().transform_size(target_geo.size);
let output_aspect_ratio = size.w as f64 / size.h as f64;
let ratio = data.aspect_ratio / output_aspect_ratio;
if ratio > 1. {
@@ -307,6 +340,10 @@ impl State {
}
}
if pressed {
self.hide_cursor_if_needed();
}
let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input(
self,
event.key_code(),
@@ -349,7 +386,10 @@ impl State {
self.handle_bind(bind.clone());
// Start the key repeat timer if necessary.
self.start_key_repeat(bind);
}
fn start_key_repeat(&mut self, bind: Bind) {
if !bind.repeat {
return;
}
@@ -383,6 +423,22 @@ impl State {
self.niri.bind_repeat_timer = Some(token);
}
fn hide_cursor_if_needed(&mut self) {
if !self.niri.config.borrow().cursor.hide_when_typing {
return;
}
// niri keeps this set only while actively using a tablet, which means the cursor position
// is likely to change almost immediately, causing pointer_hidden to just flicker back and
// forth.
if self.niri.tablet_cursor_location.is_some() {
return;
}
self.niri.pointer_hidden = true;
self.niri.queue_redraw_all();
}
pub fn handle_bind(&mut self, bind: Bind) {
let Some(cooldown) = bind.cooldown else {
self.do_action(bind.action, bind.allow_when_locked);
@@ -452,6 +508,9 @@ impl State {
Action::PowerOffMonitors => {
self.niri.deactivate_monitors(&mut self.backend);
}
Action::PowerOnMonitors => {
self.niri.activate_monitors(&mut self.backend);
}
Action::ToggleDebugTint => {
self.backend.toggle_debug_tint();
self.niri.queue_redraw_all();
@@ -482,6 +541,10 @@ impl State {
}
}
Action::ConfirmScreenshot => {
if !self.niri.screenshot_ui.is_open() {
return;
}
self.backend.with_primary_renderer(|renderer| {
match self.niri.screenshot_ui.capture(renderer) {
Ok((size, pixels)) => {
@@ -502,6 +565,10 @@ impl State {
self.niri.queue_redraw_all();
}
Action::CancelScreenshot => {
if !self.niri.screenshot_ui.is_open() {
return;
}
self.niri.screenshot_ui.close();
self.niri
.cursor_manager
@@ -529,7 +596,7 @@ impl State {
let mut windows = self.niri.layout.windows();
let window = windows.find(|(_, m)| m.id().get() == id);
if let Some((Some(monitor), mapped)) = window {
let output = &monitor.output;
let output = monitor.output();
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) {
warn!("error taking screenshot: {err:?}");
@@ -677,17 +744,39 @@ impl State {
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowLeft => {
self.niri.layout.consume_or_expel_window_left();
self.niri.layout.consume_or_expel_window_left(None);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowLeftById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri.layout.consume_or_expel_window_left(Some(&window));
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::ConsumeOrExpelWindowRight => {
self.niri.layout.consume_or_expel_window_right();
self.niri.layout.consume_or_expel_window_right(None);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowRightById(id) => {
let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
let window = window.map(|(_, m)| m.window.clone());
if let Some(window) = window {
self.niri
.layout
.consume_or_expel_window_right(Some(&window));
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
@@ -1240,12 +1329,17 @@ impl State {
self.niri.tablet_cursor_location = None;
// Check if we have an active pointer constraint.
//
// FIXME: ideally this should use the pointer focus with up-to-date global location.
let mut pointer_confined = None;
if let Some(focus) = &self.niri.pointer_focus.surface {
let pos_within_surface = pos - focus.1;
if let Some(under) = &self.niri.pointer_contents.surface {
// No need to check if the pointer focus surface matches, because here we're checking
// for an already-active constraint, and the constraint is deactivated when the focused
// surface changes.
let pos_within_surface = pos - under.1;
let mut pointer_locked = false;
with_pointer_constraint(&focus.0, &pointer, |constraint| {
with_pointer_constraint(&under.0, &pointer, |constraint| {
let Some(constraint) = constraint else { return };
if !constraint.is_active() {
return;
@@ -1263,7 +1357,7 @@ impl State {
pointer_locked = true;
}
PointerConstraint::Confined(confine) => {
pointer_confined = Some((focus.clone(), confine.region().cloned()));
pointer_confined = Some((under.clone(), confine.region().cloned()));
}
}
});
@@ -1272,7 +1366,7 @@ impl State {
if pointer_locked {
pointer.relative_motion(
self,
Some(focus.clone()),
Some(under.clone()),
&RelativeMotionEvent {
delta: event.delta(),
delta_unaccel: event.delta_unaccel(),
@@ -1330,7 +1424,7 @@ impl State {
self.niri.screenshot_ui.pointer_motion(point);
}
let under = self.niri.surface_under_and_global_space(new_pos);
let under = self.niri.contents_under(new_pos);
// Handle confined pointer.
if let Some((focus_surface, region)) = pointer_confined {
@@ -1368,10 +1462,7 @@ impl State {
self.niri.handle_focus_follows_mouse(&under);
// Activate a new confinement if necessary.
self.niri.maybe_activate_pointer_constraint(new_pos, &under);
self.niri.pointer_focus.clone_from(&under);
self.niri.pointer_contents.clone_from(&under);
pointer.motion(
self,
@@ -1395,6 +1486,9 @@ impl State {
pointer.frame(self);
// Activate a new confinement if necessary.
self.niri.maybe_activate_pointer_constraint();
// Redraw to update the cursor position.
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
@@ -1429,12 +1523,11 @@ impl State {
self.niri.screenshot_ui.pointer_motion(point);
}
let under = self.niri.surface_under_and_global_space(pos);
let under = self.niri.contents_under(pos);
self.niri.handle_focus_follows_mouse(&under);
self.niri.maybe_activate_pointer_constraint(pos, &under);
self.niri.pointer_focus.clone_from(&under);
self.niri.pointer_contents.clone_from(&under);
pointer.motion(
self,
@@ -1448,6 +1541,8 @@ impl State {
pointer.frame(self);
self.niri.maybe_activate_pointer_constraint();
// We moved the pointer, show it.
self.niri.pointer_hidden = false;
@@ -1469,11 +1564,47 @@ impl State {
let button_state = event.state();
if ButtonState::Pressed == button_state {
// We received an event for the regular pointer, so show it now.
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
if let Some(mapped) = self.niri.window_under_cursor() {
let window = mapped.window.clone();
// Check if we need to start an interactive move.
if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
};
if mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let output = output.clone();
self.niri.layout.activate_window(&window);
if self.niri.layout.interactive_move_begin(
window.clone(),
&output,
pos_within_output,
) {
let start_data = PointerGrabStartData {
focus: None,
button: event.button_code(),
location,
};
let grab = MoveGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
}
// Check if we need to start an interactive resize.
if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
@@ -1531,7 +1662,6 @@ impl State {
};
let grab = ResizeGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
self.niri.cursor_manager.set_cursor_image(
CursorImageStatus::Named(edges.cursor_icon()),
);
@@ -1567,7 +1697,6 @@ impl State {
};
let grab = SpatialMovementGrab::new(start_data, output);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
@@ -1576,11 +1705,11 @@ impl State {
}
};
self.update_pointer_focus();
self.update_pointer_contents();
if ButtonState::Pressed == button_state {
let layer_focus = self.niri.pointer_focus.layer.clone();
self.niri.focus_layer_surface_if_on_demand(layer_focus);
let layer_under = self.niri.pointer_contents.layer.clone();
self.niri.focus_layer_surface_if_on_demand(layer_under);
}
if let Some(button) = event.button() {
@@ -1623,6 +1752,12 @@ impl State {
fn on_pointer_axis<I: InputBackend>(&mut self, event: I::PointerAxisEvent) {
let source = event.source();
// We received an event for the regular pointer, so show it now. This is also needed for
// update_pointer_contents() below to return the real contents, necessary for the pointer
// axis event to reach the window.
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal);
let vertical_amount_v120 = event.amount_v120(Axis::Vertical);
@@ -1761,14 +1896,24 @@ impl State {
}
}
let scroll_factor = match source {
AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor,
AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor,
_ => None,
};
let scroll_factor = scroll_factor.map(|x| x.0).unwrap_or(1.);
let horizontal_amount = horizontal_amount.unwrap_or_else(|| {
// Winit backend, discrete scrolling.
horizontal_amount_v120.unwrap_or(0.0) / 120. * 15.
});
}) * scroll_factor;
let vertical_amount = vertical_amount.unwrap_or_else(|| {
// Winit backend, discrete scrolling.
vertical_amount_v120.unwrap_or(0.0) / 120. * 15.
});
}) * scroll_factor;
let horizontal_amount_v120 = horizontal_amount_v120.map(|x| x * scroll_factor);
let vertical_amount_v120 = vertical_amount_v120.map(|x| x * scroll_factor);
let mut frame = AxisFrame::new(event.time_msec()).source(source);
if horizontal_amount != 0.0 {
@@ -1797,7 +1942,7 @@ impl State {
}
}
self.update_pointer_focus();
self.update_pointer_contents();
let pointer = &self.niri.seat.get_pointer().unwrap();
pointer.axis(self, frame);
@@ -1812,7 +1957,7 @@ impl State {
return;
};
let under = self.niri.surface_under_and_global_space(pos);
let under = self.niri.contents_under(pos);
let tablet_seat = self.niri.seat.tablet_seat();
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
@@ -1864,7 +2009,7 @@ impl State {
tool.tip_down(serial, event.time_msec());
if let Some(pos) = self.niri.tablet_cursor_location {
let under = self.niri.surface_under_and_global_space(pos);
let under = self.niri.contents_under(pos);
if let Some(window) = under.window {
self.niri.layout.activate_window(&window);
@@ -1894,7 +2039,7 @@ impl State {
return;
};
let under = self.niri.surface_under_and_global_space(pos);
let under = self.niri.contents_under(pos);
let tablet_seat = self.niri.seat.tablet_seat();
let display_handle = self.niri.display_handle.clone();
@@ -1926,6 +2071,7 @@ impl State {
self.move_cursor(pos);
}
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
}
}
@@ -1959,7 +2105,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2050,7 +2196,7 @@ impl State {
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2093,7 +2239,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2111,7 +2257,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2128,7 +2274,7 @@ impl State {
fn on_gesture_pinch_update<I: InputBackend>(&mut self, event: I::GesturePinchUpdateEvent) {
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2147,7 +2293,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2165,7 +2311,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2183,7 +2329,7 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
if self.update_pointer_contents() {
pointer.frame(self);
}
@@ -2222,12 +2368,40 @@ impl State {
return;
};
let under = self.niri.surface_under_and_global_space(touch_location);
let serial = SERIAL_COUNTER.next_serial();
let under = self.niri.contents_under(touch_location);
if !handle.is_grabbed() {
if let Some(window) = under.window {
self.niri.layout.activate_window(&window);
// Check if we need to start an interactive move.
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
};
if mod_down {
let (output, pos_within_output) =
self.niri.output_under(touch_location).unwrap();
let output = output.clone();
if self.niri.layout.interactive_move_begin(
window.clone(),
&output,
pos_within_output,
) {
let start_data = TouchGrabStartData {
focus: None,
slot: evt.slot(),
location: touch_location,
};
let grab = TouchMoveGrab::new(start_data, window.clone());
handle.set_grab(self, grab, serial);
}
}
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = under.output {
@@ -2239,7 +2413,6 @@ impl State {
self.niri.focus_layer_surface_if_on_demand(under.layer);
};
let serial = SERIAL_COUNTER.next_serial();
handle.down(
self,
under.surface,
@@ -2275,7 +2448,7 @@ impl State {
let Some(touch_location) = self.compute_touch_location(&evt) else {
return;
};
let under = self.niri.surface_under_and_global_space(touch_location);
let under = self.niri.contents_under(touch_location);
handle.motion(
self,
under.surface,
@@ -2298,6 +2471,28 @@ impl State {
};
handle.cancel(self);
}
fn on_switch_toggle<I: InputBackend>(&mut self, evt: I::SwitchToggleEvent) {
let Some(switch) = evt.switch() else {
return;
};
if switch == Switch::Lid {
let is_closed = evt.state() == SwitchState::On;
debug!("lid switch {}", if is_closed { "closed" } else { "opened" });
self.niri.is_lid_closed = is_closed;
self.backend.on_output_config_changed(&mut self.niri);
}
let action = {
let bindings = &self.niri.config.borrow().switch_events;
find_configured_switch_action(bindings, switch, evt.state())
};
if let Some(action) = action {
self.do_action(action, true);
}
}
}
/// Check whether the key should be intercepted and mark intercepted
@@ -2305,10 +2500,10 @@ impl State {
/// to them from being delivered.
#[allow(clippy::too_many_arguments)]
fn should_intercept_key(
suppressed_keys: &mut HashSet<u32>,
suppressed_keys: &mut HashSet<Keycode>,
bindings: &Binds,
comp_mod: CompositorMod,
key_code: u32,
key_code: Keycode,
modified: Keysym,
raw: Option<Keysym>,
pressed: bool,
@@ -2449,6 +2644,23 @@ fn find_configured_bind(
None
}
fn find_configured_switch_action(
bindings: &SwitchBinds,
switch: Switch,
state: SwitchState,
) -> Option<Action> {
let switch_action = match (switch, state) {
(Switch::Lid, SwitchState::Off) => &bindings.lid_open,
(Switch::Lid, SwitchState::On) => &bindings.lid_close,
(Switch::TabletMode, SwitchState::Off) => &bindings.tablet_mode_off,
(Switch::TabletMode, SwitchState::On) => &bindings.tablet_mode_on,
_ => unreachable!(),
};
switch_action
.as_ref()
.map(|switch_action| Action::Spawn(switch_action.spawn.clone()))
}
fn modifiers_from_state(mods: ModifiersState) -> Modifiers {
let mut modifiers = Modifiers::empty();
if mods.ctrl {
@@ -2530,6 +2742,20 @@ fn should_notify_activity<I: InputBackend>(event: &InputEvent<I>) -> bool {
)
}
fn should_reset_pointer_inactivity_timer<I: InputBackend>(event: &InputEvent<I>) -> bool {
matches!(
event,
InputEvent::PointerAxis { .. }
| InputEvent::PointerButton { .. }
| InputEvent::PointerMotion { .. }
| InputEvent::PointerMotionAbsolute { .. }
| InputEvent::TabletToolAxis { .. }
| InputEvent::TabletToolButton { .. }
| InputEvent::TabletToolProximity { .. }
| InputEvent::TabletToolTip { .. }
)
}
fn allowed_when_locked(action: &Action) -> bool {
matches!(
action,
@@ -2537,6 +2763,7 @@ fn allowed_when_locked(action: &Action) -> bool {
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
| Action::PowerOnMonitors
| Action::SwitchLayout(_)
)
}
@@ -2544,7 +2771,11 @@ fn allowed_when_locked(action: &Action) -> bool {
fn allowed_during_screenshot(action: &Action) -> bool {
matches!(
action,
Action::Quit(_) | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
Action::Quit(_)
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
| Action::PowerOnMonitors
)
}
@@ -2576,8 +2807,20 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
if let Some(method) = c.scroll_method {
let _ = device.config_scroll_set_method(method.into());
if method == niri_config::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
} else if let Some(default) = device.config_scroll_default_method() {
let _ = device.config_scroll_set_method(default);
if default == input::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
}
if let Some(tap_button_map) = c.tap_button_map {
@@ -2632,8 +2875,57 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
if let Some(method) = c.scroll_method {
let _ = device.config_scroll_set_method(method.into());
if method == niri_config::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
} else if let Some(default) = device.config_scroll_default_method() {
let _ = device.config_scroll_set_method(default);
if default == input::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
}
}
if is_trackball {
let c = &config.trackball;
let _ = device.config_send_events_set_mode(if c.off {
input::SendEventsMode::DISABLED
} else {
input::SendEventsMode::ENABLED
});
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
let _ = device.config_middle_emulation_set_enabled(c.middle_emulation);
let _ = device.config_left_handed_set(c.left_handed);
if let Some(accel_profile) = c.accel_profile {
let _ = device.config_accel_set_profile(accel_profile.into());
} else if let Some(default) = device.config_accel_default_profile() {
let _ = device.config_accel_set_profile(default);
}
if let Some(method) = c.scroll_method {
let _ = device.config_scroll_set_method(method.into());
if method == niri_config::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
} else if let Some(default) = device.config_scroll_default_method() {
let _ = device.config_scroll_set_method(default);
if default == input::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
}
}
@@ -2656,8 +2948,20 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
if let Some(method) = c.scroll_method {
let _ = device.config_scroll_set_method(method.into());
if method == niri_config::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
} else if let Some(default) = device.config_scroll_default_method() {
let _ = device.config_scroll_set_method(default);
if default == input::ScrollMethod::OnButtonDown {
if let Some(button) = c.scroll_button {
let _ = device.config_scroll_set_button(button);
}
}
}
}
@@ -2754,8 +3058,8 @@ mod tests {
// The key_code we pick is arbitrary, the only thing
// that matters is that they are different between cases.
let close_key_code = close_keysym.into();
let close_key_event = |suppr: &mut HashSet<u32>, mods: ModifiersState, pressed| {
let close_key_code = Keycode::from(close_keysym.raw() + 8u32);
let close_key_event = |suppr: &mut HashSet<Keycode>, mods: ModifiersState, pressed| {
should_intercept_key(
suppr,
&bindings,
@@ -2771,12 +3075,12 @@ mod tests {
};
// Key event with the code which can't trigger any action.
let none_key_event = |suppr: &mut HashSet<u32>, mods: ModifiersState, pressed| {
let none_key_event = |suppr: &mut HashSet<Keycode>, mods: ModifiersState, pressed| {
should_intercept_key(
suppr,
&bindings,
comp_mod,
Keysym::l.into(),
Keycode::from(Keysym::l.raw() + 8),
Keysym::l,
Some(Keysym::l),
pressed,
+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);
}
}
+25 -40
View File
@@ -16,17 +16,14 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::input::keyboard::XkbContextHandler;
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
@@ -362,22 +359,12 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result
}
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
id: mapped.id().get(),
title: role.title.clone(),
app_id: role.app_id.clone(),
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
}
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(),
})
}
@@ -385,10 +372,13 @@ 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 layouts = context.keymap().layouts();
let xkb = context.xkb().lock().unwrap();
let layouts = xkb.layouts();
KeyboardLayouts {
names: layouts.map(str::to_owned).collect(),
current_idx: context.active_layout().0 as u8,
names: layouts
.map(|layout| xkb.layout_name(layout).to_owned())
.collect(),
current_idx: xkb.active_layout().0 as u8,
}
});
@@ -406,7 +396,10 @@ impl State {
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| context.active_layout().0 as u8);
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;
@@ -459,7 +452,7 @@ impl State {
// 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 != ws.name
|| ipc_ws.name.as_ref() != ws.name()
|| ipc_ws.output.as_ref() != output_name
{
need_workspaces_changed = true;
@@ -482,7 +475,7 @@ impl State {
}
// Check if this workspace became active.
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx);
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 });
}
@@ -503,9 +496,9 @@ impl State {
Workspace {
id,
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name.clone(),
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_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()),
}
@@ -546,28 +539,20 @@ impl State {
}
let Some(ipc_win) = state.windows.get(&id) else {
let window = make_ipc_window(mapped, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
};
let workspace_id = Some(ws_id.get());
let workspace_id = ws_id.map(|id| id.get());
let mut changed = ipc_win.workspace_id != workspace_id;
let wl_surface = mapped.toplevel().wl_surface();
changed |= with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
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, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
}
+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)
}
}
+1216 -166
View File
File diff suppressed because it is too large Load Diff
+210 -216
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,
@@ -20,7 +21,7 @@ use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, to_physical_precise_round, ResizeEdge};
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.;
@@ -33,19 +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)]
@@ -59,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,
@@ -105,10 +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]
}
@@ -175,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);
@@ -209,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);
@@ -230,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());
@@ -314,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();
}
@@ -403,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();
}
@@ -455,18 +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,
Transaction::new(),
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) {
@@ -483,18 +539,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,
Transaction::new(),
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, window: Option<&W::Id>, idx: usize) {
@@ -531,17 +589,19 @@ impl<W: LayoutElement> Monitor<W> {
let workspace = &mut self.workspaces[source_workspace_idx];
let column = &workspace.columns[col_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let activate = source_workspace_idx == self.active_workspace_idx
&& col_idx == workspace.active_column_idx
&& tile_idx == column.active_tile_idx;
let window = workspace
.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None)
.into_window();
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
self.add_window(new_idx, window, activate, width, is_full_width);
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();
@@ -561,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);
}
@@ -578,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);
}
@@ -595,7 +655,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);
}
@@ -673,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())
@@ -811,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 {
@@ -907,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) {
+14 -3
View File
@@ -64,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>,
@@ -74,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! {
@@ -90,7 +93,7 @@ niri_render_elements! {
}
}
type TileRenderSnapshot =
pub type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
@@ -123,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,
@@ -305,6 +309,8 @@ impl<W: LayoutElement> Tile<W> {
offset.y += move_.from * move_.anim.value();
}
offset += self.interactive_move_offset;
offset
}
@@ -364,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
}
@@ -381,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;
}
+606 -425
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -33,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() {
+225 -104
View File
@@ -17,6 +17,7 @@ use niri_config::{
DEFAULT_BACKGROUND_COLOR,
};
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::Keycode;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
@@ -43,7 +44,7 @@ use smithay::desktop::{
layer_map_for_output, LayerSurface, PopupGrab, PopupManager, PopupUngrabStrategy, Space,
Window, WindowSurfaceType,
};
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler};
use smithay::input::keyboard::Layout as KeyboardLayout;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
use smithay::input::{Seat, SeatState};
use smithay::output::{self, Output, OutputModeSource, PhysicalProperties, Subpixel};
@@ -109,12 +110,13 @@ use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
use crate::frame_clock::FrameClock;
use crate::handlers::configure_lock_surface;
use crate::handlers::{configure_lock_surface, XDG_ACTIVATION_TOKEN_TIMEOUT};
use crate::input::scroll_tracker::ScrollTracker;
use crate::input::{
apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData,
};
use crate::ipc::server::IpcServer;
use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::WorkspaceId;
use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -205,6 +207,12 @@ pub struct Niri {
// When false, we're idling with monitors powered off.
pub monitors_active: bool,
/// Whether the laptop lid is closed.
///
/// Libinput guarantees that the lid switch starts in open state, and if it was closed during
/// startup, libinput will immediately send a closed event.
pub is_lid_closed: bool,
pub devices: HashSet<input::Device>,
pub tablets: HashMap<input::Device, TabletData>,
pub touch: HashSet<input::Device>,
@@ -248,7 +256,7 @@ pub struct Niri {
pub seat: Seat<State>,
/// Scancodes of the keys to suppress.
pub suppressed_keys: HashSet<u32>,
pub suppressed_keys: HashSet<Keycode>,
pub bind_cooldown_timers: HashMap<Key, RegistrationToken>,
pub bind_repeat_timer: Option<RegistrationToken>,
pub keyboard_focus: KeyboardFocus,
@@ -259,15 +267,27 @@ pub struct Niri {
pub cursor_manager: CursorManager,
pub cursor_texture_cache: CursorTextureCache,
pub cursor_shape_manager_state: CursorShapeManagerState,
pub dnd_icon: Option<WlSurface>,
pub pointer_focus: PointerFocus,
pub dnd_icon: Option<DndIcon>,
/// Contents under pointer.
///
/// Periodically updated: on motion and other events and in the loop callback. If you require
/// the real up-to-date contents somewhere, it's better to recompute on the spot.
///
/// This is not pointer focus. I.e. during a click grab, the pointer focus remains on the
/// client with the grab, but this field will keep updating to the latest contents as if no
/// grab was active.
///
/// This is primarily useful for emitting pointer motion events for surfaces that move
/// underneath the cursor on their own (i.e. when the tiling layout moves). In this case, not
/// taking grabs into account is expected, because we pass the information to pointer.motion()
/// which passes it down through grabs, which decide what to do with it as they see fit.
pub pointer_contents: PointContents,
/// Whether the pointer is hidden, for example due to a previous touch input.
///
/// When this happens, the pointer also loses any focus. This is so that touch can prevent
/// various tooltips from sticking around.
pub pointer_hidden: bool,
// FIXME: this should be able to be removed once PointerFocus takes grabs into account.
pub pointer_grab_ongoing: bool,
pub pointer_inactivity_timer: Option<RegistrationToken>,
pub tablet_cursor_location: Option<Point<f64, Logical>>,
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
pub vertical_wheel_tracker: ScrollTracker,
@@ -304,6 +324,12 @@ pub struct Niri {
pub mapped_cast_output: HashMap<Window, Output>,
}
#[derive(Debug)]
pub struct DndIcon {
pub surface: WlSurface,
pub offset: Point<i32, Logical>,
}
pub struct OutputState {
pub global: GlobalId,
pub frame_clock: FrameClock,
@@ -377,10 +403,10 @@ pub enum KeyboardFocus {
}
#[derive(Default, Clone, PartialEq)]
pub struct PointerFocus {
// Output under pointer.
pub struct PointContents {
// Output under point.
pub output: Option<Output>,
// Surface under pointer and its location in global coordinate space.
// Surface under point and its location in the global coordinate space.
pub surface: Option<(WlSurface, Point<f64, Logical>)>,
// If surface belongs to a window, this is that window.
pub window: Option<Window>,
@@ -533,15 +559,18 @@ impl State {
self.notify_blocker_cleared();
// These should be called periodically, before flushing the clients.
self.niri.layout.refresh();
self.niri.cursor_manager.check_cursor_image_surface_alive();
self.niri.refresh_pointer_outputs();
self.niri.popups.cleanup();
self.niri.global_space.refresh();
self.niri.refresh_idle_inhibit();
self.refresh_popup_grab();
self.update_keyboard_focus();
self.refresh_pointer_focus();
// Needs to be called after updating the keyboard focus.
self.niri.refresh_layout();
self.niri.cursor_manager.check_cursor_image_surface_alive();
self.niri.refresh_pointer_outputs();
self.niri.global_space.refresh();
self.niri.refresh_idle_inhibit();
self.refresh_pointer_contents();
foreign_toplevel::refresh(self);
self.niri.refresh_window_rules();
self.refresh_ipc_outputs();
@@ -562,10 +591,8 @@ impl State {
}
pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
let under = self.niri.surface_under_and_global_space(location);
self.niri
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus.clone_from(&under);
let under = self.niri.contents_under(location);
self.niri.pointer_contents.clone_from(&under);
let pointer = &self.niri.seat.get_pointer().unwrap();
pointer.motion(
@@ -579,8 +606,9 @@ impl State {
);
pointer.frame(self);
// We moved the pointer, show it.
self.niri.pointer_hidden = false;
self.niri.maybe_activate_pointer_constraint();
// We do not show the pointer on programmatic or keyboard movement.
// FIXME: granular
self.niri.queue_redraw_all();
@@ -634,14 +662,13 @@ impl State {
let Some(output) = self.niri.layout.active_output() else {
return false;
};
let output = output.clone();
let monitor = self.niri.layout.monitor_for_output(&output).unwrap();
let monitor = self.niri.layout.monitor_for_output(output).unwrap();
let mut rv = false;
let rect = monitor.active_tile_visual_rectangle();
if let Some(rect) = rect {
let output_geo = self.niri.global_space.output_geometry(&output).unwrap();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let mut rect = rect;
rect.loc += output_geo.loc.to_f64();
rv = self.move_cursor_to_rect(rect, mode);
@@ -666,8 +693,8 @@ impl State {
self.move_cursor_to_focused_tile(CenterCoords::Both)
}
pub fn refresh_pointer_focus(&mut self) {
let _span = tracy_client::span!("Niri::refresh_pointer_focus");
pub fn refresh_pointer_contents(&mut self) {
let _span = tracy_client::span!("Niri::refresh_pointer_contents");
let pointer = &self.niri.seat.get_pointer().unwrap();
let location = pointer.current_location();
@@ -682,36 +709,37 @@ impl State {
}
}
if !self.update_pointer_focus() {
if !self.update_pointer_contents() {
return;
}
pointer.frame(self);
// Pointer motion from a surface to nothing triggers a cursor change to default, which
// means we may need to redraw.
// FIXME: granular
self.niri.queue_redraw_all();
}
pub fn update_pointer_focus(&mut self) -> bool {
let _span = tracy_client::span!("Niri::update_pointer_focus");
pub fn update_pointer_contents(&mut self) -> bool {
let _span = tracy_client::span!("Niri::update_pointer_contents");
let pointer = &self.niri.seat.get_pointer().unwrap();
let location = pointer.current_location();
let under = if self.niri.pointer_hidden {
PointerFocus::default()
PointContents::default()
} else {
self.niri.surface_under_and_global_space(location)
self.niri.contents_under(location)
};
// We're not changing the global cursor location here, so if the focus did not change, then
// nothing changed.
if self.niri.pointer_focus == under {
// We're not changing the global cursor location here, so if the contents did not change,
// then nothing changed.
if self.niri.pointer_contents == under {
return false;
}
self.niri
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus.clone_from(&under);
self.niri.pointer_contents.clone_from(&under);
pointer.motion(
self,
@@ -723,6 +751,8 @@ impl State {
},
);
self.niri.maybe_activate_pointer_constraint();
true
}
@@ -893,8 +923,10 @@ impl State {
}
if self.niri.config.borrow().input.keyboard.track_layout == TrackLayout::Window {
let current_layout =
keyboard.with_xkb_state(self, |context| context.active_layout());
let current_layout = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
xkb.active_layout()
});
let mut new_layout = current_layout;
// Store the currently active layout for the surface.
@@ -982,6 +1014,7 @@ impl State {
let mut window_rules_changed = false;
let mut debug_config_changed = false;
let mut shaders_changed = false;
let mut cursor_inactivity_timeout_changed = false;
let mut old_config = self.niri.config.borrow_mut();
// Reload the cursor.
@@ -1068,8 +1101,18 @@ impl State {
shaders_changed = true;
}
if config.cursor.hide_after_inactive_ms != old_config.cursor.hide_after_inactive_ms {
cursor_inactivity_timeout_changed = true;
}
if config.debug != old_config.debug {
debug_config_changed = true;
if config.debug.keep_laptop_panel_on_when_lid_is_closed
!= old_config.debug.keep_laptop_panel_on_when_lid_is_closed
{
output_config_changed = true;
}
}
*old_config = config;
@@ -1114,6 +1157,10 @@ impl State {
self.niri.layout.update_shaders();
}
if cursor_inactivity_timeout_changed {
self.niri.reset_pointer_inactivity_timer();
}
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
@@ -1520,7 +1567,7 @@ impl State {
to_introspect: &async_channel::Sender<NiriToIntrospect>,
msg: IntrospectToNiri,
) {
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::utils::with_toplevel_role;
let IntrospectToNiri::GetWindows = msg;
let _span = tracy_client::span!("GetWindows");
@@ -1528,21 +1575,8 @@ impl State {
let mut windows = HashMap::new();
self.niri.layout.with_windows(|mapped, _, _| {
let wl_surface = mapped
.window
.toplevel()
.expect("no X11 support")
.wl_surface();
let id = mapped.id().get();
let props = with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let props = with_toplevel_role(mapped.toplevel(), |role| {
gnome_shell_introspect::WindowProperties {
title: role.title.clone().unwrap_or_default(),
app_id: role.app_id.clone().unwrap_or_default(),
@@ -1672,6 +1706,18 @@ impl Niri {
is_tty && !client.get_data::<ClientState>().unwrap().restricted
});
let activation_state = XdgActivationState::new::<State>(&display_handle);
event_loop
.insert_source(
Timer::from_duration(XDG_ACTIVATION_TOKEN_TIMEOUT),
|_, _, state| {
state.niri.activation_state.retain_tokens(|_, token_data| {
token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
});
TimeoutAction::ToDuration(XDG_ACTIVATION_TOKEN_TIMEOUT)
},
)
.unwrap();
let mutter_x11_interop_state =
MutterX11InteropManagerState::new::<State, _>(&display_handle, move |_| true);
@@ -1775,7 +1821,7 @@ impl Niri {
.unwrap();
drop(config_);
Self {
let mut niri = Self {
config,
config_file_output_config,
@@ -1797,6 +1843,7 @@ impl Niri {
blocker_cleared_tx,
blocker_cleared_rx,
monitors_active: true,
is_lid_closed: false,
devices: HashSet::new(),
tablets: HashMap::new(),
@@ -1850,9 +1897,9 @@ impl Niri {
cursor_texture_cache: Default::default(),
cursor_shape_manager_state,
dnd_icon: None,
pointer_focus: PointerFocus::default(),
pointer_contents: PointContents::default(),
pointer_hidden: false,
pointer_grab_ongoing: false,
pointer_inactivity_timer: None,
tablet_cursor_location: None,
gesture_swipe_3f_cumulative: None,
vertical_wheel_tracker: ScrollTracker::new(120),
@@ -1887,11 +1934,19 @@ impl Niri {
#[cfg(feature = "xdp-gnome-screencast")]
mapped_cast_output: HashMap::new(),
}
};
niri.reset_pointer_inactivity_timer();
niri
}
#[cfg(feature = "dbus")]
pub fn inhibit_power_key(&mut self) -> anyhow::Result<()> {
use std::os::fd::{AsRawFd, BorrowedFd};
use smithay::reexports::rustix::io::{fcntl_setfd, FdFlags};
let conn = zbus::blocking::ConnectionBuilder::system()?.build()?;
let message = conn.call_method(
@@ -1902,7 +1957,14 @@ impl Niri {
&("handle-power-key", "niri", "Power key handling", "block"),
)?;
let fd = message.body()?;
let fd: zbus::zvariant::OwnedFd = message.body()?;
// Don't leak the fd to child processes.
let borrowed = unsafe { BorrowedFd::borrow_raw(fd.as_raw_fd()) };
if let Err(err) = fcntl_setfd(borrowed, FdFlags::CLOEXEC) {
warn!("error setting CLOEXEC on inhibit fd: {err:?}");
};
self.inhibit_power_key_fd = Some(fd);
Ok(())
@@ -2284,13 +2346,14 @@ impl Niri {
self.window_under(pos)
}
/// Returns the surface under cursor and its position in the global space.
/// Returns contents under the given point.
///
/// Pointer needs location in global space, and focused window location compatible with that
/// global space. We don't have a global space for all windows, but this function converts the
/// window location temporarily to the current global space.
pub fn surface_under_and_global_space(&mut self, pos: Point<f64, Logical>) -> PointerFocus {
let mut rv = PointerFocus::default();
/// We don't have a proper global space for all windows, so this function converts window
/// locations to global space according to where they are rendered.
///
/// This function does not take pointer or touch grabs into account.
pub fn contents_under(&mut self, pos: Point<f64, Logical>) -> PointContents {
let mut rv = PointContents::default();
let Some((output, pos_within_output)) = self.output_under(pos) else {
return rv;
@@ -2589,22 +2652,20 @@ impl Niri {
let output_scale = Scale::from(output.current_scale().fractional_scale());
let (mut pointer_elements, pointer_pos) = match render_cursor {
RenderCursor::Hidden => (vec![], pointer_pos.to_physical_precise_round(output_scale)),
let mut pointer_elements = match render_cursor {
RenderCursor::Hidden => vec![],
RenderCursor::Surface { surface, hotspot } => {
let pointer_pos =
(pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale);
let pointer_elements = render_elements_from_surface_tree(
render_elements_from_surface_tree(
renderer,
&surface,
pointer_pos,
output_scale,
1.,
Kind::Cursor,
);
(pointer_elements, pointer_pos)
)
}
RenderCursor::Named {
icon,
@@ -2620,7 +2681,7 @@ impl Niri {
let mut pointer_elements = vec![];
let pointer_element = match MemoryRenderBufferRenderElement::from_buffer(
renderer,
pointer_pos.to_f64(),
pointer_pos,
&texture,
None,
None,
@@ -2637,14 +2698,16 @@ impl Niri {
pointer_elements.push(OutputRenderElements::NamedPointer(element));
}
(pointer_elements, pointer_pos)
pointer_elements
}
};
if let Some(dnd_icon) = &self.dnd_icon {
if let Some(dnd_icon) = self.dnd_icon.as_ref() {
let pointer_pos =
(pointer_pos + dnd_icon.offset.to_f64()).to_physical_precise_round(output_scale);
pointer_elements.extend(render_elements_from_surface_tree(
renderer,
dnd_icon,
&dnd_icon.surface,
pointer_pos,
output_scale,
1.,
@@ -2667,7 +2730,7 @@ impl Niri {
.tablet_cursor_location
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
match self.cursor_manager.cursor_image().clone() {
match self.cursor_manager.cursor_image() {
CursorImageStatus::Surface(ref surface) => {
let hotspot = with_states(surface, |states| {
states
@@ -2685,6 +2748,7 @@ impl Niri {
let dnd = self
.dnd_icon
.as_ref()
.map(|icon| &icon.surface)
.map(|surface| (surface, bbox_from_surface_tree(surface, surface_pos)));
// FIXME we basically need to pick the largest scale factor across the overlapping
@@ -2746,12 +2810,12 @@ impl Niri {
}
cursor_image => {
// There's no cursor surface, but there might be a DnD icon.
let Some(surface) = &self.dnd_icon else {
let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) else {
return;
};
let icon = if let CursorImageStatus::Named(icon) = cursor_image {
icon
*icon
} else {
Default::default()
};
@@ -2800,6 +2864,23 @@ impl Niri {
}
}
pub fn refresh_layout(&mut self) {
let layout_is_active = match &self.keyboard_focus {
KeyboardFocus::Layout { .. } => true,
KeyboardFocus::LayerShell { .. } => false,
// Draw layout as active in these cases to reduce unnecessary window animations.
// There's no confusion because these are both fullscreen modes.
//
// FIXME: when going into the screenshot UI from a layer-shell focus, and then back to
// layer-shell, the layout will briefly draw as active, despite never having focus.
KeyboardFocus::LockScreen { .. } => true,
KeyboardFocus::ScreenshotUi => true,
};
self.layout.refresh(layout_is_active);
}
pub fn refresh_idle_inhibit(&mut self) {
let _span = tracy_client::span!("Niri::refresh_idle_inhibit");
@@ -3019,7 +3100,11 @@ impl Niri {
// Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap();
let monitor_elements = mon.render_elements(renderer, target);
let monitor_elements: Vec<_> = mon.render_elements(renderer, target).collect();
let float_elements: Vec<_> = self
.layout
.render_floating_for_output(renderer, output, target)
.collect();
// Get layer-shell elements.
let layer_map = layer_map_for_output(output);
@@ -3050,10 +3135,12 @@ impl Niri {
// Then the regular monitor elements and the top layer in varying order.
if mon.render_above_top_layer() {
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
extend_from_layer(&mut elements, Layer::Top);
} else {
extend_from_layer(&mut elements, Layer::Top);
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
}
@@ -3095,11 +3182,7 @@ impl Niri {
}
}
state.unfinished_animations_remain = self
.layout
.monitor_for_output(output)
.unwrap()
.are_animations_ongoing();
state.unfinished_animations_remain = self.layout.are_animations_ongoing(Some(output));
self.config_error_notification
.advance_animations(target_presentation_time);
@@ -3268,7 +3351,7 @@ impl Niri {
);
}
if let Some(surface) = &self.dnd_icon {
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
with_surface_tree_downward(
surface,
(),
@@ -3406,7 +3489,7 @@ impl Niri {
);
}
if let Some(surface) = &self.dnd_icon {
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
send_dmabuf_feedback_surface_tree(
surface,
output,
@@ -3508,7 +3591,7 @@ impl Niri {
);
}
if let Some(surface) = &self.dnd_icon {
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
send_frames_surface_tree(
surface,
output,
@@ -3576,7 +3659,7 @@ impl Niri {
}
}
if let Some(surface) = &self.dnd_icon {
if let Some(surface) = &self.dnd_icon.as_ref().map(|icon| &icon.surface) {
send_frames_surface_tree(
surface,
output,
@@ -3615,7 +3698,7 @@ impl Niri {
);
}
if let Some(surface) = &self.dnd_icon {
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
take_presentation_feedback_surface_tree(
surface,
&mut feedback,
@@ -4427,8 +4510,16 @@ impl Niri {
self.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
self.lock_state = LockState::Locking(confirmation);
self.queue_redraw_all();
if self.output_state.is_empty() {
// There are no outputs, lock the session right away.
let lock = confirmation.ext_session_lock().clone();
confirmation.lock();
self.lock_state = LockState::Locked(lock);
} else {
// There are outputs, which we need to redraw before locking.
self.lock_state = LockState::Locking(confirmation);
self.queue_redraw_all();
}
}
pub fn unlock(&mut self) {
@@ -4455,17 +4546,20 @@ impl Niri {
output_state.lock_surface = Some(surface);
}
pub fn maybe_activate_pointer_constraint(
&self,
new_pos: Point<f64, Logical>,
new_under: &PointerFocus,
) {
let Some((surface, surface_loc)) = &new_under.surface else {
/// Activates the pointer constraint if necessary according to the current pointer contents.
///
/// Make sure the pointer location and contents are up to date before calling this.
pub fn maybe_activate_pointer_constraint(&self) {
let pointer = self.seat.get_pointer().unwrap();
let pointer_pos = pointer.current_location();
let Some((surface, surface_loc)) = &self.pointer_contents.surface else {
return;
};
if self.pointer_grab_ongoing {
if Some(surface) != pointer.current_focus().as_ref() {
return;
}
let pointer = &self.seat.get_pointer().unwrap();
with_pointer_constraint(surface, pointer, |constraint| {
let Some(constraint) = constraint else { return };
@@ -4476,8 +4570,8 @@ impl Niri {
// Constraint does not apply if not within region.
if let Some(region) = constraint.region() {
let new_pos_within_surface = new_pos - *surface_loc;
if !region.contains(new_pos_within_surface.to_i32_round()) {
let pos_within_surface = pointer_pos - *surface_loc;
if !region.contains(pos_within_surface.to_i32_round()) {
return;
}
}
@@ -4549,7 +4643,7 @@ impl Niri {
}
}
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) {
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointContents) {
let Some(ffm) = self.config.borrow().input.focus_follows_mouse else {
return;
};
@@ -4560,7 +4654,7 @@ impl Niri {
}
// Recompute the current pointer focus because we don't update it during animations.
let current_focus = self.surface_under_and_global_space(pointer.current_location());
let current_focus = self.contents_under(pointer.current_location());
if let Some(output) = &new_focus.output {
if current_focus.output.as_ref() != Some(output) {
@@ -4698,6 +4792,32 @@ impl Niri {
self.queue_redraw_all();
}
}
pub fn reset_pointer_inactivity_timer(&mut self) {
let _span = tracy_client::span!("Niri::reset_pointer_inactivity_timer");
if let Some(token) = self.pointer_inactivity_timer.take() {
self.event_loop.remove(token);
}
let Some(timeout_ms) = self.config.borrow().cursor.hide_after_inactive_ms else {
return;
};
let duration = Duration::from_millis(timeout_ms as u64);
let timer = Timer::from_duration(duration);
let token = self
.event_loop
.insert_source(timer, move |_, _, state| {
state.niri.pointer_inactivity_timer = None;
state.niri.pointer_hidden = true;
state.niri.queue_redraw_all();
TimeoutAction::Drop
})
.unwrap();
self.pointer_inactivity_timer = Some(token);
}
}
pub struct ClientState {
@@ -4715,6 +4835,7 @@ impl ClientData for ClientState {
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement,
+10 -26
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;
@@ -96,37 +94,23 @@ pub fn refresh(state: &mut State) {
// 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();
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);
});
}
}
+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();
+24 -1
View File
@@ -17,8 +17,11 @@ 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;
@@ -221,6 +224,26 @@ pub fn output_matches_name(output: &Output, target: &str) -> bool {
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();
+9 -15
View File
@@ -15,7 +15,7 @@ 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, XdgToplevelSurfaceData};
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use super::{ResolvedWindowRules, WindowRef};
use crate::handlers::KdeDecorationsModeState;
@@ -33,7 +33,7 @@ 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::transaction::Transaction;
use crate::utils::{send_scale_transform, ResizeEdge};
use crate::utils::{send_scale_transform, with_toplevel_role, ResizeEdge};
#[derive(Debug)]
pub struct Mapped {
@@ -571,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,
@@ -631,14 +631,7 @@ impl LayoutElement for Mapped {
let _span =
trace_span!("configure_intent", surface = ?self.toplevel().wl_surface().id()).entered();
with_states(self.toplevel().wl_surface(), |states| {
let attributes = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
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 {
@@ -719,10 +712,11 @@ impl LayoutElement for Mapped {
}
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 {
+4 -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;
@@ -144,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());
@@ -169,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)) {
+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
+29
View File
@@ -22,6 +22,8 @@ debug {
emulate-zero-presentation-time
disable-resize-throttling
disable-transactions
keep-laptop-panel-on-when-lid-is-closed
disable-monitor-names
}
binds {
@@ -165,6 +167,33 @@ debug {
}
```
### `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.
+26
View File
@@ -42,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
@@ -303,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.
+1
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)
+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"; }
}
```
+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`.
+7
View File
@@ -46,3 +46,10 @@ 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>
+13 -15
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).
@@ -178,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.
@@ -195,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
+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
+1
View File
@@ -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)