Compare commits

..

65 Commits

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

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

* Expand docs

---------

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

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

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

* Added dinit support to niri-session

* Replaced shutdown script for dinit with a single command execution

* Added dinit service files to Getting Started install tables

* Fix typo in resources/dinit/niri

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

* Fixed mistakes in wiki/Getting-Started.md

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

* niri-session does not start dinit anymore

---------

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

* Remove redundant fully qualified path

* Find root surface

* Convert nesting to if-return

* Manually wrap error messages

* Remove error!() prints

* Add queue redraw

---------

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

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

- Removing some unnecessary dependencies of the package itself
- Allowing for overriding the package
- Adding Cargo feature toggles
- Installing all niri-related resources
- Avoiding `LD_LIBRARY_PATH` hacks
2024-10-05 12:09:24 -07:00
seth 3ab3e778ab flake: drop most external inputs
Previously, inputs like Crane and Fenix were used to only build the
`niri` package. This isn't really required, and can easily be replaced
by nixpkgs' `rustPlatform` -- which will also lead to less dependencies
being pulled into user's lockfiles
2024-10-05 12:09:24 -07:00
seth e6203313ce flake: format with nixfmt 2024-10-05 12:09:24 -07:00
seth 938061dd5e flake: use nixfmt 2024-10-05 12:09:24 -07:00
Ivan Molodetskikh 0cca7a2116 default-config: Add more comments to prefer-no-csd 2024-10-01 13:28:28 +03:00
Ivan Molodetskikh 39b46b3326 default-config: Add rounded corner window rule example 2024-10-01 13:28:16 +03:00
Ivan Molodetskikh 2aebd6bdbb default-config: Add comments to consume/expel binds 2024-10-01 13:20:38 +03:00
Ivan Molodetskikh b501a9b303 Upgrade dependencies 2024-09-30 15:27:36 +03:00
Ivan Molodetskikh 94e5408f46 Update Smithay 2024-09-30 15:24:50 +03:00
Christian Meissl eb190e3f94 handle role specific buffer offset 2024-09-30 05:04:58 -07:00
spazzylemons 80bb0d5876 Remove one unnecessary .clone() call and reorder another 2024-09-30 00:45:44 -07:00
Marwin Kreuzig c04ccafd0a fix focus_up_or_right 2024-09-28 05:18:22 -07:00
sodiboo 6ee5b5afa7 flake: update inputs and remove crane.inputs.nixpkgs override
the input was removed in https://github.com/ipetkov/crane/pull/692
2024-09-15 08:05:05 -07:00
37 changed files with 4168 additions and 1611 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
+335 -353
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -11,15 +11,15 @@ repository = "https://github.com/YaLTeR/niri"
rust-version = "1.77"
[workspace.dependencies]
anyhow = "1.0.88"
anyhow = "1.0.90"
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_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,31 +50,31 @@ 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"
drm-ffi = "0.9.0"
fastrand = "2.1.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
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"] }
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.158"
libc = "0.2.161"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.9", path = "niri-config" }
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "~4.10.0", optional = true }
ordered-float = "4.2.2"
pango = { version = "0.20.1", features = ["v1_44"] }
pangocairo = "0.20.1"
ordered-float = "4.4.0"
pango = { version = "0.20.4", features = ["v1_44"] }
pangocairo = "0.20.4"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.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
@@ -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]
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,
libseat,
libxkbcommon,
mesa,
pango,
pipewire,
pkg-config,
rustPlatform,
systemd,
wayland,
withDbus ? true,
withSystemd ? true,
withScreencastSupport ? true,
withDinit ? false,
}:
craneArgs = {
rustPlatform.buildRustPackage {
pname = "niri";
version = self.rev or "dirty";
version = self.shortRev or self.dirtyShortRev or "unknown";
src = nixpkgs.lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(builtins.match "resources" path == null)
|| ((craneLib.filterCargoSources path type)
&& (builtins.match "niri-visual-tests" path == null));
src = nix-filter.lib.filter {
root = self;
include = [
"niri-config"
"niri-ipc"
"niri-visual-tests"
"resources"
"src"
./Cargo.lock
./Cargo.toml
];
};
nativeBuildInputs = with pkgs; [
pkg-config
autoPatchelfHook
postPatch = ''
patchShebangs resources/niri-session
substituteInPlace resources/niri.service \
--replace-fail '/usr/bin' "$out/bin"
'';
cargoLock = {
# NOTE: This is only used for Git dependencies
allowBuiltinFetchGit = true;
lockFile = ./Cargo.lock;
};
strictDeps = true;
nativeBuildInputs = [
clang
gdk-pixbuf
graphene
gtk4
libadwaita
pkg-config
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libdisplay-info
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
cairo
glib
pixman
];
buildInputs =
[
cairo
dbus
libGL
libdisplay-info
libinput
libseat
libxkbcommon
mesa # libgbm
pango
wayland
]
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
++ lib.optional withScreencastSupport pipewire
# Also includes libudev
++ lib.optional withSystemd systemd;
runtimeDependencies = with pkgs; [
wayland
mesa
libglvnd # For libEGL
xorg.libXcursor
xorg.libXi
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()`
CARGO_BUILD_RUSTFLAGS = toString (
map (arg: "-C link-arg=" + arg) [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
]
);
};
passthru = {
providedSessions = [ "niri" ];
};
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/YaLTeR/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
};
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
in {
formatter = pkgs.alejandra;
inherit (nixpkgs) lib;
# Support all Linux systems that the nixpkgs flake exposes
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
checks.niri = niri;
packages.default = niri;
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
{
checks = forAllSystems (system: {
# We use the debug build here to save a bit of time
inherit (self.packages.${system}) niri-debug;
});
devShells.default = 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"`
inherit (niri) CARGO_BUILD_RUSTFLAGS;
};
};
}
);
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
packages = forAllSystems (
system:
let
niri = nixpkgsFor.${system}.callPackage niri-package { };
in
{
inherit niri;
# NOTE: This is for development purposes only
#
# It is primarily to help with quickly iterating on
# changes made to the above expression - though it is
# also not stripped in order to better debug niri itself
niri-debug = niri.overrideAttrs (
newAttrs: oldAttrs: {
pname = oldAttrs.pname + "-debug";
cargoBuildType = "debug";
cargoCheckType = newAttrs.cargoBuildType;
dontStrip = true;
}
);
default = niri;
}
);
overlays.default = final: _: {
niri = final.callPackage niri-package { };
};
};
}
+2 -2
View File
@@ -13,7 +13,7 @@ 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"
regex = "1.11.0"
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"
+154 -2
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)]
@@ -196,6 +202,8 @@ 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)]
@@ -214,6 +222,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 +421,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 +444,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 +591,23 @@ 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,
}
impl Default for InsertHint {
fn default() -> Self {
Self {
off: false,
color: Color::from_rgba8_unpremul(127, 200, 255, 128),
}
}
}
/// RGB color in [0, 1] with unpremultiplied alpha.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Color {
@@ -615,6 +665,10 @@ pub struct Cursor {
pub xcursor_theme: String,
#[knuffel(child, unwrap(argument), default = 24)]
pub xcursor_size: u8,
#[knuffel(child)]
pub hide_on_key_press: bool,
#[knuffel(child, unwrap(argument))]
pub hide_after_inactive_ms: Option<u32>,
}
impl Default for Cursor {
@@ -622,6 +676,8 @@ impl Default for Cursor {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
hide_on_key_press: false,
hide_after_inactive_ms: None,
}
}
}
@@ -1054,6 +1110,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 +1136,7 @@ pub enum Action {
ChangeVt(i32),
Suspend,
PowerOffMonitors,
PowerOnMonitors,
ToggleDebugTint,
DebugToggleOpaqueRegions,
DebugToggleDamage,
@@ -1115,7 +1190,11 @@ pub enum Action {
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeOrExpelWindowLeft,
#[knuffel(skip)]
ConsumeOrExpelWindowLeftById(u64),
ConsumeOrExpelWindowRight,
#[knuffel(skip)]
ConsumeOrExpelWindowRightById(u64),
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
@@ -1176,6 +1255,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 +1301,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,
@@ -2862,6 +2952,7 @@ 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
}
@@ -2871,6 +2962,7 @@ mod tests {
accel-speed 0.4
accel-profile "flat"
scroll-method "no-scroll"
scroll-button 273
middle-emulation
}
@@ -2880,6 +2972,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 +3048,10 @@ mod tests {
}
center-focused-column "on-overflow"
insert-hint {
color "rgb(255, 200, 127)"
}
}
spawn-at-startup "alacritty" "-e" "fish"
@@ -2953,6 +3061,8 @@ mod tests {
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 16
hide-on-key-press
hide-after-inactive-ms 3000
}
screenshot-path "~/Screenshots/screenshot.png"
@@ -3013,6 +3123,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,6 +3160,7 @@ 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,
@@ -3056,6 +3172,7 @@ 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,
},
@@ -3065,8 +3182,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 +3250,10 @@ mod tests {
active_gradient: None,
inactive_gradient: None,
},
insert_hint: InsertHint {
off: false,
color: Color::from_rgba8_unpremul(255, 200, 127, 255),
},
preset_column_widths: vec![
PresetSize::Proportion(0.25),
PresetSize::Proportion(0.5),
@@ -3154,6 +3286,8 @@ mod tests {
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
hide_on_key_press: true,
hide_after_inactive_ms: Some(3000),
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
hotkey_overlay: HotkeyOverlay {
@@ -3345,6 +3479,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()
+26 -4
View File
@@ -132,6 +132,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 +242,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.
+1 -1
View File
@@ -10,7 +10,7 @@ repository.workspace = true
[dependencies]
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.1", package = "gtk4", features = ["v4_12"] }
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.9", path = ".." }
niri-config = { version = "0.1.9", path = "../niri-config" }
smithay.workspace = true
+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()
}
+22 -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.
+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
+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
+15 -20
View File
@@ -1019,6 +1019,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 +1058,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(())
@@ -1659,10 +1666,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:?}");
}
}
}
@@ -2154,17 +2160,6 @@ fn get_drm_property(
.find_map(|(handle, value)| (handle == prop).then_some(value))
}
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
return;
};
let value = property::Value::Boolean(active);
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
warn!("error setting CRTC property: {err:?}");
}
}
fn refresh_interval(mode: DrmMode) -> Duration {
let clock = mode.clock() as u64;
let htotal = mode.hsync().2 as u64;
+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;
+78 -7
View File
@@ -12,7 +12,9 @@ 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 +24,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 +35,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 +66,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,
};
@@ -141,6 +143,59 @@ impl PointerConstraintsHandler for State {
&self.niri.pointer_focus,
);
}
fn cursor_position_hint(
&mut self,
surface: &WlSurface,
pointer: &PointerHandle<Self>,
location: Point<f64, Logical>,
) {
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
constraint.map_or(false, |c| c.is_active())
});
if !is_constraint_active {
return;
}
// Logically the following two checks should always succeed (so, they should print
// error!()s if they fail). However, currently both can fail because niri's pointer focus
// doesn't take pointer grabs into account. So if you start, say, a middle-drag in Blender,
// then touchpad-swipe the window away, the niri pointer focus will change, even though the
// real pointer focus remains on the Blender surface due to the click grab.
//
// FIXME: add error!()s when niri pointer focus takes grabs into account. Alternatively,
// recompute the surface origin here (but that is a bit clunky).
let Some((ref focused_surface, origin)) = self.niri.pointer_focus.surface else {
return;
};
if focused_surface != surface {
return;
}
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
let target = self
.niri
.output_for_root(&root)
.and_then(|output| self.niri.global_space.output_geometry(output))
.map_or(origin + location, |mut output_geometry| {
// i32 sizes are exclusive, but f64 sizes are inclusive.
output_geometry.size -= (1, 1).into();
(origin + location).constrain(output_geometry.to_f64())
});
pointer.set_location(target);
// Redraw to update the cursor position if it's visible.
if !self.niri.pointer_hidden {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
}
}
delegate_pointer_constraints!(State);
@@ -225,7 +280,23 @@ impl ClientDndGrabHandler for State {
icon: Option<WlSurface>,
_seat: Seat<Self>,
) {
self.niri.dnd_icon = icon;
let offset = if let CursorImageStatus::Surface(ref surface) =
self.niri.cursor_manager.cursor_image()
{
with_states(surface, |states| {
let hotspot = states
.data_map
.get::<CursorImageSurfaceData>()
.unwrap()
.lock()
.unwrap()
.hotspot;
Point::from((-hotspot.x, -hotspot.y))
})
} else {
(0, 0).into()
};
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
// FIXME: more granular
self.niri.queue_redraw_all();
}
@@ -403,7 +474,7 @@ impl ForeignToplevelHandler for State {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
.move_to_output(Some(&window), &requested_output, None);
}
}
+153 -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,96 @@ impl XdgShellHandler for State {
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
let wl_surface = surface.wl_surface();
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
pointer.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab = grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Pointer(start_data.clone()));
}
}
}
}
});
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
touch.with_grab(|grab_serial, grab| {
if grab_serial == serial {
let start_data = grab.start_data();
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
// Deny move requests from DnD grabs to work around
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
let is_dnd_grab =
grab.as_any().downcast_ref::<DnDGrab<Self>>().is_some();
if !is_dnd_grab {
grab_start_data =
Some(PointerOrTouchStartData::Touch(start_data.clone()));
}
}
}
}
});
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output_pos = self
.niri
.global_space
.output_geometry(&output)
.unwrap()
.loc
.to_f64();
let pos_within_output = start_data.location() - output_pos;
if !self
.niri
.layout
.interactive_move_begin(window.clone(), &output, pos_within_output)
{
return;
}
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchMoveGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
self.niri.queue_redraw(&output);
}
fn resize_request(
@@ -76,24 +168,39 @@ impl XdgShellHandler for State {
serial: Serial,
edges: xdg_toplevel::ResizeEdge,
) {
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
let mut grab_start_data = None;
// See if this comes from a pointer grab.
let pointer = self.niri.seat.get_pointer().unwrap();
if pointer.has_grab(serial) {
if let Some(start_data) = pointer.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
}
}
}
}
// See if this comes from a touch grab.
if let Some(touch) = self.niri.seat.get_touch() {
if touch.has_grab(serial) {
if let Some(start_data) = touch.grab_start_data() {
if let Some((focus, _)) = &start_data.focus {
if focus.id().same_client_as(&wl_surface.id()) {
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
}
}
}
}
}
let Some(start_data) = grab_start_data else {
return;
};
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
@@ -128,14 +235,26 @@ impl XdgShellHandler for State {
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
if !self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = ResizeGrab::new(start_data, window);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
PointerOrTouchStartData::Touch(start_data) => {
let touch = self.niri.seat.get_touch().unwrap();
let grab = TouchResizeGrab::new(start_data, window);
touch.set_grab(self, grab, serial);
}
}
}
fn reposition_request(
@@ -261,7 +380,7 @@ impl XdgShellHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&surface) {
if surface.is_initial_configure_sent() {
surface.send_configure();
}
}
@@ -288,7 +407,7 @@ impl XdgShellHandler for State {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
.move_to_output(Some(&window), &requested_output, None);
}
}
@@ -332,7 +451,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
@@ -416,7 +535,7 @@ impl XdgShellHandler for State {
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let ws = workspace_name
@@ -552,7 +671,7 @@ impl XdgDecorationHandler for State {
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
if toplevel.is_initial_configure_sent() {
toplevel.send_configure();
}
}
@@ -565,7 +684,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 +739,6 @@ impl XdgForeignHandler for State {
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
})
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
@@ -706,7 +813,7 @@ impl State {
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
.map(|(mon, _)| mon.output().clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
@@ -759,7 +866,7 @@ impl State {
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
workspace_name: ws.and_then(|w| w.name().cloned()),
};
toplevel.send_configure();
@@ -788,16 +895,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();
+281 -36
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,15 @@ 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 self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::niri::State;
@@ -35,10 +40,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 +61,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 +109,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 +149,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 +265,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 +298,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 +339,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 +385,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 +422,22 @@ impl State {
self.niri.bind_repeat_timer = Some(token);
}
fn hide_cursor_if_needed(&mut self) {
if !self.niri.config.borrow().cursor.hide_on_key_press {
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 +507,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();
@@ -529,7 +587,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 +735,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();
@@ -1469,11 +1549,48 @@ 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.pointer_grab_ongoing = true;
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,
@@ -1618,6 +1735,8 @@ impl State {
},
);
pointer.frame(self);
self.niri.refresh_layout_is_grabbed();
}
fn on_pointer_axis<I: InputBackend>(&mut self, event: I::PointerAxisEvent) {
@@ -2253,6 +2372,8 @@ impl State {
// We're using touch, hide the pointer.
self.niri.pointer_hidden = true;
self.niri.refresh_layout_is_grabbed();
}
fn on_touch_up<I: InputBackend>(&mut self, evt: I::TouchUpEvent) {
let Some(handle) = self.niri.seat.get_touch() else {
@@ -2298,6 +2419,21 @@ impl State {
};
handle.cancel(self);
}
fn on_switch_toggle<I: InputBackend>(&mut self, evt: I::SwitchToggleEvent) {
let Some(switch) = evt.switch() else {
return;
};
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 +2441,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 +2585,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 +2683,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 +2704,7 @@ fn allowed_when_locked(action: &Action) -> bool {
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
| Action::PowerOnMonitors
| Action::SwitchLayout(_)
)
}
@@ -2544,7 +2712,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 +2748,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 +2816,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 +2889,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 +2999,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 +3016,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,
+225
View File
@@ -0,0 +1,225 @@
use std::time::Duration;
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
is_moving: false,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for MoveGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
// FIXME: only redraw the previous and the new output.
data.niri.queue_redraw_all();
return;
}
} else {
return;
}
}
// The move is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// TODO: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
details: AxisFrame,
) {
handle.axis(data, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &PointerGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+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);
}
}
+17 -12
View File
@@ -16,7 +16,6 @@ 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;
@@ -385,10 +384,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 +408,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 +464,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 +487,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 +508,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,12 +551,12 @@ 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();
@@ -567,7 +572,7 @@ impl State {
});
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;
}
+1096 -145
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) {
+13 -2
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
}
+639 -428
View File
File diff suppressed because it is too large Load Diff
+136 -40
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};
@@ -115,6 +116,7 @@ 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};
@@ -248,7 +250,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,13 +261,14 @@ pub struct Niri {
pub cursor_manager: CursorManager,
pub cursor_texture_cache: CursorTextureCache,
pub cursor_shape_manager_state: CursorShapeManagerState,
pub dnd_icon: Option<WlSurface>,
pub dnd_icon: Option<DndIcon>,
pub pointer_focus: PointerFocus,
/// 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,
pub pointer_inactivity_timer: Option<RegistrationToken>,
// FIXME: this should be able to be removed once PointerFocus takes grabs into account.
pub pointer_grab_ongoing: bool,
pub tablet_cursor_location: Option<Point<f64, Logical>>,
@@ -304,6 +307,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,
@@ -533,14 +542,17 @@ 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();
// 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_focus();
foreign_toplevel::refresh(self);
self.niri.refresh_window_rules();
@@ -581,6 +593,7 @@ impl State {
// We moved the pointer, show it.
self.niri.pointer_hidden = false;
self.niri.reset_pointer_inactivity_timer();
// FIXME: granular
self.niri.queue_redraw_all();
@@ -634,14 +647,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);
@@ -893,8 +905,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 +996,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,6 +1083,10 @@ 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;
}
@@ -1114,6 +1133,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
@@ -1775,7 +1798,7 @@ impl Niri {
.unwrap();
drop(config_);
Self {
let mut niri = Self {
config,
config_file_output_config,
@@ -1852,6 +1875,7 @@ impl Niri {
dnd_icon: None,
pointer_focus: PointerFocus::default(),
pointer_hidden: false,
pointer_inactivity_timer: None,
pointer_grab_ongoing: false,
tablet_cursor_location: None,
gesture_swipe_3f_cumulative: None,
@@ -1887,11 +1911,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 +1934,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(())
@@ -2589,22 +2628,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 +2657,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 +2674,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 +2706,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 +2724,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 +2786,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 +2840,33 @@ impl Niri {
}
}
pub fn refresh_layout_is_grabbed(&mut self) {
let pointer = self.seat.get_pointer().unwrap();
let touch = self.seat.get_touch();
let touch_is_grabbed = touch.map_or(false, |touch| touch.is_grabbed());
self.layout
.set_pointer_grabbed(pointer.is_grabbed() || touch_is_grabbed);
}
pub fn refresh_layout(&mut self) {
self.refresh_layout_is_grabbed();
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 +3086,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 +3121,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 +3168,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 +3337,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 +3475,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 +3577,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 +3645,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 +3684,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,
@@ -4698,6 +4767,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 +4810,7 @@ impl ClientData for ClientState {
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement,
+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();
+3 -1
View File
@@ -56,6 +56,7 @@ input {
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// middle-emulation
}
@@ -134,13 +135,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`: 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:
+24
View File
@@ -42,6 +42,11 @@ layout {
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
}
insert-hint {
// off
color "#ffc87f80"
}
struts {
// left 64
// right 64
@@ -303,6 +308,25 @@ 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` lets you change the color of the hint and has the same syntax as colors in border and focus ring.
```kdl
layout {
insert-hint {
// off
color "#ffc87f80"
}
}
```
### `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-on-key-press
hide-after-inactive-ms 1000
}
hotkey-overlay {
@@ -106,6 +109,31 @@ cursor {
}
```
#### `hide-on-key-press`
<sup>Since: 0.1.10</sup>
If set, hides the cursor when pressing a key on the keyboard.
```kdl
cursor {
hide-on-key-press
}
```
#### `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)
+45
View File
@@ -0,0 +1,45 @@
### Overview
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 "bash" "-c" "niri msg output \"eDP-1\" off"; }
lid-open { spawn "bash" "-c" "niri msg output \"eDP-1\" on"; }
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.
You could use them to turn the laptop internal monitor off and on (until niri gets this functionality built-in).
```kdl
switch-events {
lid-close { spawn "bash" "-c" "niri msg output \"eDP-1\" off"; }
lid-open { spawn "bash" "-c" "niri msg output \"eDP-1\" on"; }
}
```
### `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"; }
}
```
+6
View File
@@ -4,6 +4,12 @@ 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.
#### Interactive Resize
<sup>Since: 0.1.6</sup>
+10 -6
View File
@@ -8,8 +8,8 @@ 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.
You can also run `niri` inside an existing desktop session.
Then it will open as a window, where you can give it a try.
@@ -178,8 +178,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 +197,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
+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)