mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d8f640503 | |||
| b18cfbae23 | |||
| f64e7e14c3 | |||
| e8c9bfc06a | |||
| 07452f50a8 | |||
| 642c5acebb | |||
| 0886dedff1 | |||
| cc88a7d42e | |||
| c0829087da | |||
| b6f6d6a7c2 | |||
| 5ff8b89aaf | |||
| 927abad4b4 | |||
| 3d31f9860a | |||
| 8867a4f84c | |||
| 88f4c1d610 | |||
| ddcb5c5e10 | |||
| cd90dfc7be | |||
| a778ab3897 | |||
| 4c2f49d566 | |||
| 49d7052bb3 | |||
| 07be7e7eae | |||
| 97c8717d1e | |||
| 3ac0a751fe | |||
| 8b39f986d9 | |||
| 354c365a03 | |||
| e0ebf1bdff | |||
| 11633aef98 | |||
| 9193245871 | |||
| 7baf10b751 | |||
| f5d91c5ecc | |||
| 69e3edb5a3 | |||
| d58bb4eaa3 | |||
| c5fe25f422 | |||
| 600cffb009 | |||
| b9d14a9eda | |||
| 0e7e398df3 | |||
| 86bdc6898b | |||
| e5ca335115 | |||
| fce5d66878 | |||
| 05d218113c | |||
| ef6af6adc1 | |||
| 6632699e00 | |||
| d3e72245b0 | |||
| 13fe9c8ac3 | |||
| 6ecbf2db8a | |||
| c9be9056ef | |||
| 0866990b7d | |||
| f04befb567 | |||
| da3e5c4424 | |||
| 26ab4dfb87 | |||
| e887ee93a3 | |||
| d640e85158 | |||
| c8044a9b5d | |||
| 289ae3604d | |||
| 55fb885256 | |||
| 73a531f8bc | |||
| 10f04fd19d | |||
| 79fd309d6c | |||
| dd8b2be044 | |||
| 8d08782eba | |||
| 8555f37dbf | |||
| 4b837f429c | |||
| a480087618 | |||
| 84655d3b26 | |||
| 40843cbda1 | |||
| a13b9298c6 | |||
| 0c5e046820 | |||
| 907ebc4977 | |||
| e4161be1bf | |||
| be7fbd418f | |||
| 06ec9eecdb | |||
| 79eef5ee90 | |||
| 29602ca995 | |||
| d7156df842 | |||
| 33b39913c7 | |||
| d5cbc35811 | |||
| a038c5aaab | |||
| c9c985c927 | |||
| 859c0be0e5 | |||
| 810ea245f9 | |||
| 58fc5f3b06 | |||
| 7d4e99b760 | |||
| ab7d81aae0 | |||
| e24723125f | |||
| 03c603918d | |||
| 6fb60dacd2 | |||
| 42a9daec9d | |||
| 1ba2be3928 | |||
| 66be000410 | |||
| 5fc669c282 | |||
| 9b78b15ba5 | |||
| b9fd0a405e | |||
| 1b44e0cd20 | |||
| b3d4d4eacc | |||
| a835bdc940 | |||
| b258fd69d2 | |||
| 3ab3e778ab | |||
| e6203313ce | |||
| 938061dd5e | |||
| 0cca7a2116 | |||
| 39b46b3326 | |||
| 2aebd6bdbb | |||
| b501a9b303 | |||
| 94e5408f46 | |||
| eb190e3f94 | |||
| 80bb0d5876 | |||
| c04ccafd0a | |||
| 6ee5b5afa7 |
@@ -192,7 +192,7 @@ jobs:
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
publish-wiki:
|
||||
|
||||
Generated
+614
-398
File diff suppressed because it is too large
Load Diff
+27
-25
@@ -2,7 +2,7 @@
|
||||
members = ["niri-visual-tests"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -11,15 +11,15 @@ repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.77"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.88"
|
||||
anyhow = "1.0.93"
|
||||
bitflags = "2.6.0"
|
||||
clap = { version = "4.5.17", features = ["derive"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
k9 = "0.12.0"
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.17.3", default-features = false }
|
||||
tracy-client = { version = "0.17.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
@@ -50,38 +50,38 @@ async-channel = "2.3.1"
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.18.0", features = ["derive"] }
|
||||
bytemuck = { version = "1.19.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
drm-ffi = "0.8.0"
|
||||
fastrand = "2.1.1"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.2.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.29.0"
|
||||
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||
glam = "0.29.2"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.158"
|
||||
libc = "0.2.162"
|
||||
libdisplay-info = "0.1.0"
|
||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.9", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.9", path = "niri-ipc", features = ["clap"] }
|
||||
niri-config = { version = "0.1.10", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.10", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "~4.10.0", optional = true }
|
||||
ordered-float = "4.2.2"
|
||||
pango = { version = "0.20.1", features = ["v1_44"] }
|
||||
pangocairo = "0.20.1"
|
||||
ordered-float = "4.5.0"
|
||||
pango = { version = "0.20.4", features = ["v1_44"] }
|
||||
pangocairo = "0.20.4"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.13"
|
||||
portable-atomic = { version = "1.7.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.15"
|
||||
sd-notify = "0.4.2"
|
||||
png = "0.17.14"
|
||||
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.3"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.2", optional = true }
|
||||
url = { version = "2.5.3", optional = true }
|
||||
wayland-backend = "0.3.7"
|
||||
wayland-scanner = "0.31.5"
|
||||
xcursor = "0.3.8"
|
||||
@@ -109,7 +109,7 @@ features = [
|
||||
approx = "0.5.1"
|
||||
k9.workspace = true
|
||||
proptest = "1.5.0"
|
||||
proptest-derive = "0.5.0"
|
||||
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
|
||||
xshell = "0.2.6"
|
||||
|
||||
[features]
|
||||
@@ -124,6 +124,8 @@ xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
# Enables the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
profile-with-tracy-allocations = ["profile-with-tracy"]
|
||||
# Enables dinit integration (global environment).
|
||||
dinit = []
|
||||
|
||||
@@ -137,7 +139,7 @@ lto = "thin"
|
||||
debug = false
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
Generated
+18
-92
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,101 +4,248 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix/monthly";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
crane,
|
||||
nix-filter,
|
||||
flake-utils,
|
||||
fenix,
|
||||
...
|
||||
}: let
|
||||
systems = ["aarch64-linux" "x86_64-linux"];
|
||||
in
|
||||
flake-utils.lib.eachSystem systems (
|
||||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
toolchain = fenix.packages.${system}.complete.toolchain;
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-filter,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
cairo,
|
||||
clang,
|
||||
dbus,
|
||||
libGL,
|
||||
libclang,
|
||||
libdisplay-info,
|
||||
libinput,
|
||||
seatd,
|
||||
libxkbcommon,
|
||||
mesa,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
withDbus ? true,
|
||||
withSystemd ? true,
|
||||
withScreencastSupport ? true,
|
||||
withDinit ? false,
|
||||
}:
|
||||
|
||||
craneArgs = {
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.rev or "dirty";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
|
||||
src = nixpkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = path: type:
|
||||
(builtins.match "resources" path == null)
|
||||
|| ((craneLib.filterCargoSources path type)
|
||||
&& (builtins.match "niri-visual-tests" path == null));
|
||||
src = nix-filter.lib.filter {
|
||||
root = self;
|
||||
include = [
|
||||
"niri-config"
|
||||
"niri-ipc"
|
||||
"niri-visual-tests"
|
||||
"resources"
|
||||
"src"
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
autoPatchelfHook
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
# NOTE: This is only used for Git dependencies
|
||||
allowBuiltinFetchGit = true;
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
clang
|
||||
gdk-pixbuf
|
||||
graphene
|
||||
gtk4
|
||||
libadwaita
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
wayland
|
||||
systemd # For libudev
|
||||
seatd # For libseat
|
||||
libxkbcommon
|
||||
libdisplay-info
|
||||
libinput
|
||||
mesa # For libgbm
|
||||
fontconfig
|
||||
stdenv.cc.cc.lib
|
||||
pipewire
|
||||
pango
|
||||
cairo
|
||||
glib
|
||||
pixman
|
||||
];
|
||||
buildInputs =
|
||||
[
|
||||
cairo
|
||||
dbus
|
||||
libGL
|
||||
libdisplay-info
|
||||
libinput
|
||||
seatd
|
||||
libxkbcommon
|
||||
mesa # libgbm
|
||||
pango
|
||||
wayland
|
||||
]
|
||||
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
|
||||
++ lib.optional withScreencastSupport pipewire
|
||||
# Also includes libudev
|
||||
++ lib.optional withSystemd systemd;
|
||||
|
||||
runtimeDependencies = with pkgs; [
|
||||
wayland
|
||||
mesa
|
||||
libglvnd # For libEGL
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
libxkbcommon
|
||||
];
|
||||
buildFeatures =
|
||||
lib.optional withDbus "dbus"
|
||||
++ lib.optional withDinit "dinit"
|
||||
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
|
||||
++ lib.optional withSystemd "systemd";
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath craneArgs.runtimeDependencies; # Needed for tests to find libxkbcommon
|
||||
postInstall =
|
||||
''
|
||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
LIBCLANG_PATH = lib.getLib libclang + "/lib";
|
||||
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
passthru = {
|
||||
providedSessions = [ "niri" ];
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
|
||||
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
|
||||
in {
|
||||
formatter = pkgs.alejandra;
|
||||
inherit (nixpkgs) lib;
|
||||
# Support all Linux systems that the nixpkgs flake exposes
|
||||
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
|
||||
|
||||
checks.niri = niri;
|
||||
packages.default = niri;
|
||||
forAllSystems = lib.genAttrs systems;
|
||||
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
checks = forAllSystems (system: {
|
||||
# We use the debug build here to save a bit of time
|
||||
inherit (self.packages.${system}) niri-debug;
|
||||
});
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
inputsFrom = [niri];
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (craneArgs.runtimeDependencies ++ craneArgs.nativeBuildInputs ++ craneArgs.buildInputs);
|
||||
inherit (niri) LIBCLANG_PATH;
|
||||
};
|
||||
}
|
||||
);
|
||||
nativeBuildInputs = [
|
||||
pkgs.clang
|
||||
pkgs.pkg-config
|
||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
buildInputs = niri.buildInputs ++ [
|
||||
pkgs.libadwaita # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
env = {
|
||||
inherit (niri) LIBCLANG_PATH;
|
||||
|
||||
# WARN: Do not overwrite this variable in your shell!
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
niri = nixpkgsFor.${system}.callPackage niri-package { };
|
||||
in
|
||||
{
|
||||
inherit niri;
|
||||
|
||||
# NOTE: This is for development purposes only
|
||||
#
|
||||
# It is primarily to help with quickly iterating on
|
||||
# changes made to the above expression - though it is
|
||||
# also not stripped in order to better debug niri itself
|
||||
niri-debug = niri.overrideAttrs (
|
||||
newAttrs: oldAttrs: {
|
||||
pname = oldAttrs.pname + "-debug";
|
||||
|
||||
cargoBuildType = "debug";
|
||||
cargoCheckType = newAttrs.cargoBuildType;
|
||||
|
||||
dontStrip = true;
|
||||
}
|
||||
);
|
||||
|
||||
default = niri;
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: _: {
|
||||
niri = final.callPackage niri-package { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ bitflags.workspace = true
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
niri-ipc = { version = "0.1.9", path = "../niri-ipc" }
|
||||
regex = "1.10.6"
|
||||
niri-ipc = { version = "0.1.10", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
@@ -21,4 +21,4 @@ tracy-client.workspace = true
|
||||
[dev-dependencies]
|
||||
k9.workspace = true
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
+188
-6
@@ -54,6 +54,8 @@ pub struct Config {
|
||||
#[knuffel(child, default)]
|
||||
pub binds: Binds,
|
||||
#[knuffel(child, default)]
|
||||
pub switch_events: SwitchBinds,
|
||||
#[knuffel(child, default)]
|
||||
pub debug: DebugConfig,
|
||||
#[knuffel(children(name = "workspace"))]
|
||||
pub workspaces: Vec<Workspace>,
|
||||
@@ -70,6 +72,8 @@ pub struct Input {
|
||||
#[knuffel(child, default)]
|
||||
pub trackpoint: Trackpoint,
|
||||
#[knuffel(child, default)]
|
||||
pub trackball: Trackball,
|
||||
#[knuffel(child, default)]
|
||||
pub tablet: Tablet,
|
||||
#[knuffel(child, default)]
|
||||
pub touch: Touch,
|
||||
@@ -174,6 +178,8 @@ pub struct Touchpad {
|
||||
pub accel_profile: Option<AccelProfile>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub scroll_method: Option<ScrollMethod>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub scroll_button: Option<u32>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub tap_button_map: Option<TapButtonMap>,
|
||||
#[knuffel(child)]
|
||||
@@ -182,6 +188,8 @@ pub struct Touchpad {
|
||||
pub disabled_on_external_mouse: bool,
|
||||
#[knuffel(child)]
|
||||
pub middle_emulation: bool,
|
||||
#[knuffel(child, unwrap(argument), default = FloatOrInt(1.0))]
|
||||
pub scroll_factor: FloatOrInt<0, 100>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
@@ -196,10 +204,14 @@ pub struct Mouse {
|
||||
pub accel_profile: Option<AccelProfile>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub scroll_method: Option<ScrollMethod>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub scroll_button: Option<u32>,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
#[knuffel(child)]
|
||||
pub middle_emulation: bool,
|
||||
#[knuffel(child, unwrap(argument), default = FloatOrInt(1.0))]
|
||||
pub scroll_factor: FloatOrInt<0, 100>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
@@ -214,6 +226,28 @@ pub struct Trackpoint {
|
||||
pub accel_profile: Option<AccelProfile>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub scroll_method: Option<ScrollMethod>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub scroll_button: Option<u32>,
|
||||
#[knuffel(child)]
|
||||
pub middle_emulation: bool,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
pub struct Trackball {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child)]
|
||||
pub natural_scroll: bool,
|
||||
#[knuffel(child, unwrap(argument), default)]
|
||||
pub accel_speed: f64,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub accel_profile: Option<AccelProfile>,
|
||||
#[knuffel(child, unwrap(argument, str))]
|
||||
pub scroll_method: Option<ScrollMethod>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub scroll_button: Option<u32>,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
#[knuffel(child)]
|
||||
pub middle_emulation: bool,
|
||||
}
|
||||
@@ -391,6 +425,8 @@ pub struct Layout {
|
||||
pub focus_ring: FocusRing,
|
||||
#[knuffel(child, default)]
|
||||
pub border: Border,
|
||||
#[knuffel(child, default)]
|
||||
pub insert_hint: InsertHint,
|
||||
#[knuffel(child, unwrap(children), default)]
|
||||
pub preset_column_widths: Vec<PresetSize>,
|
||||
#[knuffel(child)]
|
||||
@@ -412,6 +448,7 @@ impl Default for Layout {
|
||||
Self {
|
||||
focus_ring: Default::default(),
|
||||
border: Default::default(),
|
||||
insert_hint: Default::default(),
|
||||
preset_column_widths: Default::default(),
|
||||
default_column_width: Default::default(),
|
||||
center_focused_column: Default::default(),
|
||||
@@ -558,6 +595,26 @@ impl From<FocusRing> for Border {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct InsertHint {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, default = Self::default().color)]
|
||||
pub color: Color,
|
||||
#[knuffel(child)]
|
||||
pub gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
impl Default for InsertHint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
color: Color::from_rgba8_unpremul(127, 200, 255, 128),
|
||||
gradient: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RGB color in [0, 1] with unpremultiplied alpha.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct Color {
|
||||
@@ -615,6 +672,10 @@ pub struct Cursor {
|
||||
pub xcursor_theme: String,
|
||||
#[knuffel(child, unwrap(argument), default = 24)]
|
||||
pub xcursor_size: u8,
|
||||
#[knuffel(child)]
|
||||
pub hide_when_typing: bool,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub hide_after_inactive_ms: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
@@ -622,6 +683,8 @@ impl Default for Cursor {
|
||||
Self {
|
||||
xcursor_theme: String::from("default"),
|
||||
xcursor_size: 24,
|
||||
hide_when_typing: false,
|
||||
hide_after_inactive_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1117,24 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct SwitchBinds {
|
||||
#[knuffel(child)]
|
||||
pub lid_open: Option<SwitchAction>,
|
||||
#[knuffel(child)]
|
||||
pub lid_close: Option<SwitchAction>,
|
||||
#[knuffel(child)]
|
||||
pub tablet_mode_on: Option<SwitchAction>,
|
||||
#[knuffel(child)]
|
||||
pub tablet_mode_off: Option<SwitchAction>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||
pub struct SwitchAction {
|
||||
#[knuffel(child, unwrap(arguments))]
|
||||
pub spawn: Vec<String>,
|
||||
}
|
||||
|
||||
// Remember to add new actions to the CLI enum too.
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||
pub enum Action {
|
||||
@@ -1062,6 +1143,7 @@ pub enum Action {
|
||||
ChangeVt(i32),
|
||||
Suspend,
|
||||
PowerOffMonitors,
|
||||
PowerOnMonitors,
|
||||
ToggleDebugTint,
|
||||
DebugToggleOpaqueRegions,
|
||||
DebugToggleDamage,
|
||||
@@ -1115,7 +1197,11 @@ pub enum Action {
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
MoveWindowUpOrToWorkspaceUp,
|
||||
ConsumeOrExpelWindowLeft,
|
||||
#[knuffel(skip)]
|
||||
ConsumeOrExpelWindowLeftById(u64),
|
||||
ConsumeOrExpelWindowRight,
|
||||
#[knuffel(skip)]
|
||||
ConsumeOrExpelWindowRightById(u64),
|
||||
ConsumeWindowIntoColumn,
|
||||
ExpelWindowFromColumn,
|
||||
CenterColumn,
|
||||
@@ -1176,6 +1262,7 @@ impl From<niri_ipc::Action> for Action {
|
||||
match value {
|
||||
niri_ipc::Action::Quit { skip_confirmation } => Self::Quit(skip_confirmation),
|
||||
niri_ipc::Action::PowerOffMonitors {} => Self::PowerOffMonitors,
|
||||
niri_ipc::Action::PowerOnMonitors {} => Self::PowerOnMonitors,
|
||||
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
|
||||
niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms),
|
||||
niri_ipc::Action::Screenshot {} => Self::Screenshot,
|
||||
@@ -1221,8 +1308,18 @@ impl From<niri_ipc::Action> for Action {
|
||||
Self::MoveWindowDownOrToWorkspaceDown
|
||||
}
|
||||
niri_ipc::Action::MoveWindowUpOrToWorkspaceUp {} => Self::MoveWindowUpOrToWorkspaceUp,
|
||||
niri_ipc::Action::ConsumeOrExpelWindowLeft {} => Self::ConsumeOrExpelWindowLeft,
|
||||
niri_ipc::Action::ConsumeOrExpelWindowRight {} => Self::ConsumeOrExpelWindowRight,
|
||||
niri_ipc::Action::ConsumeOrExpelWindowLeft { id: None } => {
|
||||
Self::ConsumeOrExpelWindowLeft
|
||||
}
|
||||
niri_ipc::Action::ConsumeOrExpelWindowLeft { id: Some(id) } => {
|
||||
Self::ConsumeOrExpelWindowLeftById(id)
|
||||
}
|
||||
niri_ipc::Action::ConsumeOrExpelWindowRight { id: None } => {
|
||||
Self::ConsumeOrExpelWindowRight
|
||||
}
|
||||
niri_ipc::Action::ConsumeOrExpelWindowRight { id: Some(id) } => {
|
||||
Self::ConsumeOrExpelWindowRightById(id)
|
||||
}
|
||||
niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
|
||||
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
|
||||
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
|
||||
@@ -1436,6 +1533,10 @@ pub struct DebugConfig {
|
||||
pub disable_resize_throttling: bool,
|
||||
#[knuffel(child)]
|
||||
pub disable_transactions: bool,
|
||||
#[knuffel(child)]
|
||||
pub keep_laptop_panel_on_when_lid_is_closed: bool,
|
||||
#[knuffel(child)]
|
||||
pub disable_monitor_names: bool,
|
||||
}
|
||||
|
||||
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -1817,13 +1918,17 @@ impl OutputName {
|
||||
if self.make.is_none() && self.model.is_none() && self.serial.is_none() {
|
||||
self.connector.to_string()
|
||||
} else {
|
||||
let make = self.make.as_deref().unwrap_or("Unknown");
|
||||
let model = self.model.as_deref().unwrap_or("Unknown");
|
||||
let serial = self.serial.as_deref().unwrap_or("Unknown");
|
||||
format!("{make} {model} {serial}")
|
||||
self.format_make_model_serial()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_make_model_serial(&self) -> String {
|
||||
let make = self.make.as_deref().unwrap_or("Unknown");
|
||||
let model = self.model.as_deref().unwrap_or("Unknown");
|
||||
let serial = self.serial.as_deref().unwrap_or("Unknown");
|
||||
format!("{make} {model} {serial}")
|
||||
}
|
||||
|
||||
pub fn matches(&self, target: &str) -> bool {
|
||||
// Match by connector.
|
||||
if target.eq_ignore_ascii_case(&self.connector) {
|
||||
@@ -2862,8 +2967,10 @@ mod tests {
|
||||
accel-speed 0.2
|
||||
accel-profile "flat"
|
||||
scroll-method "two-finger"
|
||||
scroll-button 272
|
||||
tap-button-map "left-middle-right"
|
||||
disabled-on-external-mouse
|
||||
scroll-factor 0.9
|
||||
}
|
||||
|
||||
mouse {
|
||||
@@ -2871,7 +2978,9 @@ mod tests {
|
||||
accel-speed 0.4
|
||||
accel-profile "flat"
|
||||
scroll-method "no-scroll"
|
||||
scroll-button 273
|
||||
middle-emulation
|
||||
scroll-factor 0.2
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
@@ -2880,6 +2989,18 @@ mod tests {
|
||||
accel-speed 0.0
|
||||
accel-profile "flat"
|
||||
scroll-method "on-button-down"
|
||||
scroll-button 274
|
||||
}
|
||||
|
||||
trackball {
|
||||
off
|
||||
natural-scroll
|
||||
accel-speed 0.0
|
||||
accel-profile "flat"
|
||||
scroll-method "edge"
|
||||
scroll-button 275
|
||||
left-handed
|
||||
middle-emulation
|
||||
}
|
||||
|
||||
tablet {
|
||||
@@ -2944,6 +3065,11 @@ mod tests {
|
||||
}
|
||||
|
||||
center-focused-column "on-overflow"
|
||||
|
||||
insert-hint {
|
||||
color "rgb(255, 200, 127)"
|
||||
gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view"
|
||||
}
|
||||
}
|
||||
|
||||
spawn-at-startup "alacritty" "-e" "fish"
|
||||
@@ -2953,6 +3079,8 @@ mod tests {
|
||||
cursor {
|
||||
xcursor-theme "breeze_cursors"
|
||||
xcursor-size 16
|
||||
hide-when-typing
|
||||
hide-after-inactive-ms 3000
|
||||
}
|
||||
|
||||
screenshot-path "~/Screenshots/screenshot.png"
|
||||
@@ -3013,6 +3141,11 @@ mod tests {
|
||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||
}
|
||||
|
||||
switch-events {
|
||||
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
|
||||
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
|
||||
}
|
||||
|
||||
debug {
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
}
|
||||
@@ -3045,10 +3178,12 @@ mod tests {
|
||||
accel_speed: 0.2,
|
||||
accel_profile: Some(AccelProfile::Flat),
|
||||
scroll_method: Some(ScrollMethod::TwoFinger),
|
||||
scroll_button: Some(272),
|
||||
tap_button_map: Some(TapButtonMap::LeftMiddleRight),
|
||||
left_handed: false,
|
||||
disabled_on_external_mouse: true,
|
||||
middle_emulation: false,
|
||||
scroll_factor: FloatOrInt(0.9),
|
||||
},
|
||||
mouse: Mouse {
|
||||
off: false,
|
||||
@@ -3056,8 +3191,10 @@ mod tests {
|
||||
accel_speed: 0.4,
|
||||
accel_profile: Some(AccelProfile::Flat),
|
||||
scroll_method: Some(ScrollMethod::NoScroll),
|
||||
scroll_button: Some(273),
|
||||
left_handed: false,
|
||||
middle_emulation: true,
|
||||
scroll_factor: FloatOrInt(0.2),
|
||||
},
|
||||
trackpoint: Trackpoint {
|
||||
off: true,
|
||||
@@ -3065,8 +3202,19 @@ mod tests {
|
||||
accel_speed: 0.0,
|
||||
accel_profile: Some(AccelProfile::Flat),
|
||||
scroll_method: Some(ScrollMethod::OnButtonDown),
|
||||
scroll_button: Some(274),
|
||||
middle_emulation: false,
|
||||
},
|
||||
trackball: Trackball {
|
||||
off: true,
|
||||
natural_scroll: true,
|
||||
accel_speed: 0.0,
|
||||
accel_profile: Some(AccelProfile::Flat),
|
||||
scroll_method: Some(ScrollMethod::Edge),
|
||||
scroll_button: Some(275),
|
||||
left_handed: true,
|
||||
middle_emulation: true,
|
||||
},
|
||||
tablet: Tablet {
|
||||
off: false,
|
||||
map_to_output: Some("eDP-1".to_owned()),
|
||||
@@ -3122,6 +3270,20 @@ mod tests {
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
},
|
||||
insert_hint: InsertHint {
|
||||
off: false,
|
||||
color: Color::from_rgba8_unpremul(255, 200, 127, 255),
|
||||
gradient: Some(Gradient {
|
||||
from: Color::from_rgba8_unpremul(10, 20, 30, 255),
|
||||
to: Color::from_rgba8_unpremul(0, 128, 255, 255),
|
||||
angle: 180,
|
||||
relative_to: GradientRelativeTo::WorkspaceView,
|
||||
in_: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
preset_column_widths: vec![
|
||||
PresetSize::Proportion(0.25),
|
||||
PresetSize::Proportion(0.5),
|
||||
@@ -3154,6 +3316,8 @@ mod tests {
|
||||
cursor: Cursor {
|
||||
xcursor_theme: String::from("breeze_cursors"),
|
||||
xcursor_size: 16,
|
||||
hide_when_typing: true,
|
||||
hide_after_inactive_ms: Some(3000),
|
||||
},
|
||||
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
|
||||
hotkey_overlay: HotkeyOverlay {
|
||||
@@ -3345,6 +3509,24 @@ mod tests {
|
||||
allow_when_locked: false,
|
||||
},
|
||||
]),
|
||||
switch_events: SwitchBinds {
|
||||
lid_open: None,
|
||||
lid_close: None,
|
||||
tablet_mode_on: Some(SwitchAction {
|
||||
spawn: vec![
|
||||
"bash".to_owned(),
|
||||
"-c".to_owned(),
|
||||
"gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true".to_owned(),
|
||||
],
|
||||
}),
|
||||
tablet_mode_off: Some(SwitchAction {
|
||||
spawn: vec![
|
||||
"bash".to_owned(),
|
||||
"-c".to_owned(),
|
||||
"gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false".to_owned(),
|
||||
],
|
||||
}),
|
||||
},
|
||||
debug: DebugConfig {
|
||||
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
|
||||
..Default::default()
|
||||
|
||||
+5
-1
@@ -1,12 +1,16 @@
|
||||
[package]
|
||||
name = "niri-ipc"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
description = "Types and helpers for interfacing with the niri Wayland compositor."
|
||||
keywords = ["wayland"]
|
||||
categories = ["api-bindings", "os"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "0.8.21", optional = true }
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
This crate follows the niri version.
|
||||
It is **not** API-stable in terms of the Rust semver.
|
||||
In particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
|
||||
Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=0.1.10"
|
||||
```
|
||||
+40
-4
@@ -19,6 +19,20 @@
|
||||
//!
|
||||
//! This crate follows the niri version. It is **not** API-stable in terms of the Rust semver. In
|
||||
//! particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
//!
|
||||
//! Use an exact version requirement to avoid breaking changes:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=0.1.10"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! This crate defines the following features:
|
||||
//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
|
||||
//! the types.
|
||||
//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -132,6 +146,8 @@ pub enum Action {
|
||||
},
|
||||
/// Power off all monitors via DPMS.
|
||||
PowerOffMonitors {},
|
||||
/// Power on all monitors via DPMS.
|
||||
PowerOnMonitors {},
|
||||
/// Spawn a command.
|
||||
Spawn {
|
||||
/// Command to spawn.
|
||||
@@ -240,10 +256,30 @@ pub enum Action {
|
||||
MoveWindowDownOrToWorkspaceDown {},
|
||||
/// Move the focused window up in a column or to the workspace above.
|
||||
MoveWindowUpOrToWorkspaceUp {},
|
||||
/// Consume or expel the focused window left.
|
||||
ConsumeOrExpelWindowLeft {},
|
||||
/// Consume or expel the focused window right.
|
||||
ConsumeOrExpelWindowRight {},
|
||||
/// Consume or expel a window left.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Consume or expel the focused window left")
|
||||
)]
|
||||
ConsumeOrExpelWindowLeft {
|
||||
/// Id of the window to consume or expel.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Consume or expel a window right.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Consume or expel the focused window right")
|
||||
)]
|
||||
ConsumeOrExpelWindowRight {
|
||||
/// Id of the window to consume or expel.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn {},
|
||||
/// Expel the focused window from the column.
|
||||
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.1", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.9", path = ".." }
|
||||
niri-config = { version = "0.1.9", path = "../niri-config" }
|
||||
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.10", path = ".." }
|
||||
niri-config = { version = "0.1.10", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,16 @@ input {
|
||||
// scroll-method "no-scroll"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
@@ -202,7 +212,9 @@ layout {
|
||||
|
||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||
// If the client will specifically ask for CSD, the request will be honored.
|
||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
|
||||
// This option will also fix border/focus ring drawing behind some semitransparent windows.
|
||||
// After enabling or disabling this, you need to restart the apps for this to take effect.
|
||||
// prefer-no-csd
|
||||
|
||||
// You can change the path where screenshots are saved.
|
||||
@@ -250,6 +262,13 @@ window-rule {
|
||||
// block-out-from "screencast"
|
||||
}
|
||||
|
||||
// Example: enable rounded corners for all windows.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
}
|
||||
|
||||
binds {
|
||||
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||
// in the end. To find an XKB name for a particular key, you may use a program
|
||||
@@ -422,7 +441,9 @@ binds {
|
||||
// Switches focus between the current and the previous workspace.
|
||||
// Mod+Tab { focus-workspace-previous; }
|
||||
|
||||
// Consume one window from the right into the focused column.
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
// Expel one window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// There are also commands that consume or expel a single window to the side.
|
||||
@@ -465,6 +486,7 @@ binds {
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete { quit; }
|
||||
|
||||
// Powers off the monitors. To turn them back on, do any input like
|
||||
// moving the mouse or pressing any other key.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -1,3 +1,4 @@
|
||||
[preferred]
|
||||
default=gnome;gtk;
|
||||
org.freedesktop.impl.portal.Access=gtk;
|
||||
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
||||
|
||||
+47
-27
@@ -11,31 +11,51 @@ if [ -n "$SHELL" ] &&
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
# Try to detect the service manager that is being used
|
||||
if hash systemctl &> /dev/null; then
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
elif hash dinitctl &> /dev/null; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u $(id -u) dinit &> /dev/null; then
|
||||
echo "dinit user daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri &> /dev/null; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
|
||||
+107
-48
@@ -61,7 +61,7 @@ use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
|
||||
|
||||
@@ -636,22 +636,31 @@ impl Tty {
|
||||
connector,
|
||||
crtc: Some(crtc),
|
||||
} => {
|
||||
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
let connector_name = format_connector_name(&connector);
|
||||
let output_name =
|
||||
make_output_name(&device.drm, connector.handle(), connector_name, false);
|
||||
debug!(
|
||||
"new connector: {} \"{}\"",
|
||||
&output_name.connector,
|
||||
output_name.format_make_model_serial(),
|
||||
);
|
||||
|
||||
// Assign an id to this crtc.
|
||||
device.output_ids.insert(crtc, OutputId::next());
|
||||
}
|
||||
DrmScanEvent::Disconnected {
|
||||
crtc: Some(crtc), ..
|
||||
} => {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
removed.push(crtc);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this is better done in connector_disconnected(), but currently we call that to
|
||||
// turn off outputs temporarily, too. So we can't do this there.
|
||||
for crtc in &removed {
|
||||
self.connector_disconnected(niri, node, *crtc);
|
||||
}
|
||||
|
||||
let Some(device) = self.devices.get_mut(&node) else {
|
||||
error!("device disappeared");
|
||||
return;
|
||||
@@ -663,7 +672,12 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_ipc_outputs(niri);
|
||||
// This will connect any new connectors if needed, and apply other changes, such as
|
||||
// connecting back the internal laptop monitor once it becomes the only monitor left.
|
||||
//
|
||||
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
|
||||
// above.
|
||||
self.on_output_config_changed(niri);
|
||||
}
|
||||
|
||||
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
|
||||
@@ -749,7 +763,12 @@ impl Tty {
|
||||
|
||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
|
||||
let output_name = make_output_name(
|
||||
&device.drm,
|
||||
connector.handle(),
|
||||
connector_name.clone(),
|
||||
self.config.borrow().debug.disable_monitor_names,
|
||||
);
|
||||
|
||||
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
|
||||
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
|
||||
@@ -767,10 +786,6 @@ impl Tty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// This should be unique per CRTC connection, however currently we can call
|
||||
// connector_connected() multiple times for turning the output off and on.
|
||||
device.output_ids.entry(crtc).or_insert_with(OutputId::next);
|
||||
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -779,11 +794,6 @@ impl Tty {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if config.off {
|
||||
debug!("output is disabled in the config");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for m in connector.modes() {
|
||||
trace!("{m:?}");
|
||||
}
|
||||
@@ -1019,6 +1029,14 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
// Some buggy monitors replug upon powering off, so powering on here would prevent such
|
||||
// monitors from powering off. Therefore, we avoid unconditionally powering on.
|
||||
if !niri.monitors_active {
|
||||
if let Err(err) = compositor.clear() {
|
||||
warn!("error clearing drm surface: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let vblank_frame_name =
|
||||
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
|
||||
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
|
||||
@@ -1050,15 +1068,14 @@ impl Tty {
|
||||
|
||||
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
|
||||
|
||||
// Some buggy monitors replug upon powering off, so powering on here would prevent such
|
||||
// monitors from powering off. Therefore, we avoid unconditionally powering on.
|
||||
if niri.monitors_active {
|
||||
// Redraw the new monitor.
|
||||
niri.event_loop.insert_idle(move |state| {
|
||||
state.niri.queue_redraw(&output);
|
||||
// Guard against output disconnecting before the idle has a chance to run.
|
||||
if state.niri.output_state.contains_key(&output) {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
set_crtc_active(&device.drm, crtc, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1557,8 +1574,12 @@ impl Tty {
|
||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||
let connector_name = format_connector_name(connector);
|
||||
let physical_size = connector.size();
|
||||
let output_name =
|
||||
make_output_name(&device.drm, connector.handle(), connector_name.clone());
|
||||
let output_name = make_output_name(
|
||||
&device.drm,
|
||||
connector.handle(),
|
||||
connector_name.clone(),
|
||||
self.config.borrow().debug.disable_monitor_names,
|
||||
);
|
||||
|
||||
let surface = device.surfaces.get(&crtc);
|
||||
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
||||
@@ -1659,10 +1680,9 @@ impl Tty {
|
||||
}
|
||||
|
||||
for device in self.devices.values_mut() {
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
set_crtc_active(&device.drm, *crtc, false);
|
||||
if let Err(err) = surface.compositor.reset_state() {
|
||||
warn!("error resetting surface state: {err:?}");
|
||||
for surface in device.surfaces.values_mut() {
|
||||
if let Err(err) = surface.compositor.clear() {
|
||||
warn!("error clearing drm surface: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1705,6 +1725,24 @@ impl Tty {
|
||||
}
|
||||
self.update_output_config_on_resume = false;
|
||||
|
||||
// Figure out if we should disable laptop panels.
|
||||
let mut disable_laptop_panels = false;
|
||||
if niri.is_lid_closed {
|
||||
let config = self.config.borrow();
|
||||
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
|
||||
// Check if any external monitor is connected.
|
||||
'outer: for device in self.devices.values() {
|
||||
for (connector, _crtc) in device.drm_scanner.crtcs() {
|
||||
if !is_laptop_panel(&format_connector_name(connector)) {
|
||||
disable_laptop_panels = true;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
|
||||
|
||||
let mut to_disconnect = vec![];
|
||||
let mut to_connect = vec![];
|
||||
|
||||
@@ -1717,7 +1755,7 @@ impl Tty {
|
||||
.find(&surface.name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if config.off {
|
||||
if config.off || should_disable(&surface.name.connector) {
|
||||
to_disconnect.push((node, crtc));
|
||||
continue;
|
||||
}
|
||||
@@ -1813,12 +1851,21 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if already enabled.
|
||||
if device.surfaces.contains_key(&crtc) {
|
||||
if device.surfaces.contains_key(&crtc)
|
||||
|| device
|
||||
.non_desktop_connectors
|
||||
.contains(&(connector.handle(), crtc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let connector_name = format_connector_name(connector);
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
let output_name = make_output_name(
|
||||
&device.drm,
|
||||
connector.handle(),
|
||||
connector_name,
|
||||
self.config.borrow().debug.disable_monitor_names,
|
||||
);
|
||||
let config = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -1827,8 +1874,8 @@ impl Tty {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if !config.off {
|
||||
to_connect.push((node, connector.clone(), crtc));
|
||||
if !(config.off || should_disable(&output_name.connector)) {
|
||||
to_connect.push((node, connector.clone(), crtc, output_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1837,7 +1884,11 @@ impl Tty {
|
||||
self.connector_disconnected(niri, node, crtc);
|
||||
}
|
||||
|
||||
for (node, connector, crtc) in to_connect {
|
||||
// Sort by output name to get more predictable first focused output at initial compositor
|
||||
// startup, when multiple connectors appear at once.
|
||||
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
|
||||
|
||||
for (node, connector, crtc, _name) in to_connect {
|
||||
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
@@ -1872,12 +1923,21 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Check if already enabled.
|
||||
if device.surfaces.contains_key(&crtc) {
|
||||
if device.surfaces.contains_key(&crtc)
|
||||
|| device
|
||||
.non_desktop_connectors
|
||||
.contains(&(connector.handle(), crtc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let connector_name = format_connector_name(connector);
|
||||
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
let output_name = make_output_name(
|
||||
&device.drm,
|
||||
connector.handle(),
|
||||
connector_name,
|
||||
self.config.borrow().debug.disable_monitor_names,
|
||||
);
|
||||
if output_name.matches(target) {
|
||||
return Some(output_name);
|
||||
}
|
||||
@@ -2154,17 +2214,6 @@ fn get_drm_property(
|
||||
.find_map(|(handle, value)| (handle == prop).then_some(value))
|
||||
}
|
||||
|
||||
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
|
||||
let Some((prop, _, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let value = property::Value::Boolean(active);
|
||||
if let Err(err) = drm.set_property(crtc, prop, value.into()) {
|
||||
warn!("error setting CRTC property: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_interval(mode: DrmMode) -> Duration {
|
||||
let clock = mode.clock() as u64;
|
||||
let htotal = mode.hsync().2 as u64;
|
||||
@@ -2486,7 +2535,17 @@ fn make_output_name(
|
||||
device: &DrmDevice,
|
||||
connector: connector::Handle,
|
||||
connector_name: String,
|
||||
disable_monitor_names: bool,
|
||||
) -> OutputName {
|
||||
if disable_monitor_names {
|
||||
return OutputName {
|
||||
connector: connector_name,
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
};
|
||||
}
|
||||
|
||||
let info = get_edid_info(device, connector)
|
||||
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
|
||||
.ok();
|
||||
|
||||
@@ -8,6 +8,7 @@ use zbus::{dbus_interface, fdo, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -63,7 +64,7 @@ impl DisplayConfig {
|
||||
.map(|output| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -297,13 +297,52 @@ impl CompositorHandler for State {
|
||||
&self.niri.cursor_manager.cursor_image(),
|
||||
CursorImageStatus::Surface(s) if s == &root_surface
|
||||
) {
|
||||
// In case the cursor surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the cursor image hotspot
|
||||
if surface == &root_surface {
|
||||
with_states(surface, |states| {
|
||||
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
|
||||
|
||||
if let Some(mut cursor_image_attributes) =
|
||||
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
|
||||
{
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
if let Some(buffer_delta) = buffer_delta {
|
||||
cursor_image_attributes.hotspot -= buffer_delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a DnD icon surface.
|
||||
if self.niri.dnd_icon.as_ref() == Some(&root_surface) {
|
||||
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
|
||||
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
|
||||
|
||||
// In case the dnd surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the dnd icon offset
|
||||
if surface == &dnd_icon.surface {
|
||||
with_states(&dnd_icon.surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
dnd_icon.offset += buffer_delta;
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
|
||||
+99
-21
@@ -7,12 +7,15 @@ use std::io::Write;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::TabletToolDescriptor;
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||
};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
@@ -22,8 +25,8 @@ use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Rectangle, Size};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
@@ -33,7 +36,7 @@ use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
@@ -64,7 +67,7 @@ use smithay::{
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::niri::{ClientState, DndIcon, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
@@ -72,12 +75,14 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::utils::{output_size, send_scale_transform};
|
||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||
use crate::{
|
||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||
delegate_output_management, delegate_screencopy,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
impl SeatHandler for State {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
@@ -135,11 +140,66 @@ impl TabletSeatHandler for State {
|
||||
delegate_tablet_manager!(State);
|
||||
|
||||
impl PointerConstraintsHandler for State {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
self.niri.maybe_activate_pointer_constraint(
|
||||
pointer.current_location(),
|
||||
&self.niri.pointer_focus,
|
||||
);
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
|
||||
// Pointer constraints track pointer focus internally, so make sure it's up to date before
|
||||
// activating a new one.
|
||||
self.refresh_pointer_contents();
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
}
|
||||
|
||||
fn cursor_position_hint(
|
||||
&mut self,
|
||||
surface: &WlSurface,
|
||||
pointer: &PointerHandle<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
||||
constraint.map_or(false, |c| c.is_active())
|
||||
});
|
||||
|
||||
if !is_constraint_active {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
|
||||
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
|
||||
// will change, even though the real pointer focus remains on the Blender surface due to
|
||||
// the click grab.
|
||||
//
|
||||
// Ideally we would just use the constraint surface, but we need its origin. So this is
|
||||
// more of a hack because pointer contents has the surface origin available.
|
||||
//
|
||||
// FIXME: use the constraint surface somehow, don't use pointer contents.
|
||||
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
|
||||
return;
|
||||
};
|
||||
|
||||
if surface_under_pointer != surface {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
let target = self
|
||||
.niri
|
||||
.output_for_root(&root)
|
||||
.and_then(|output| self.niri.global_space.output_geometry(output))
|
||||
.map_or(origin + location, |mut output_geometry| {
|
||||
// i32 sizes are exclusive, but f64 sizes are inclusive.
|
||||
output_geometry.size -= (1, 1).into();
|
||||
(origin + location).constrain(output_geometry.to_f64())
|
||||
});
|
||||
pointer.set_location(target);
|
||||
|
||||
// Redraw to update the cursor position if it's visible.
|
||||
if !self.niri.pointer_hidden {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_pointer_constraints!(State);
|
||||
@@ -225,7 +285,23 @@ impl ClientDndGrabHandler for State {
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
) {
|
||||
self.niri.dnd_icon = icon;
|
||||
let offset = if let CursorImageStatus::Surface(ref surface) =
|
||||
self.niri.cursor_manager.cursor_image()
|
||||
{
|
||||
with_states(surface, |states| {
|
||||
let hotspot = states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot;
|
||||
Point::from((-hotspot.x, -hotspot.y))
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
@@ -388,12 +464,12 @@ impl ForeignToplevelHandler for State {
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||
{
|
||||
if !mapped
|
||||
.toplevel()
|
||||
.current_state()
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
{
|
||||
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
|
||||
role.current
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
});
|
||||
if !has_fullscreen_cap {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -403,7 +479,7 @@ impl ForeignToplevelHandler for State {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,16 +627,18 @@ impl XdgActivationHandler for State {
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
_token: XdgActivationToken,
|
||||
token: XdgActivationToken,
|
||||
token_data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
if token_data.timestamp.elapsed().as_secs() < 10 {
|
||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
self.niri.activation_state.remove_token(&token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+150
-55
@@ -23,12 +23,13 @@ use smithay::wayland::compositor::{
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::{self, Layer};
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
||||
XdgShellState, XdgToplevelSurfaceData,
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
XdgToplevelSurfaceData,
|
||||
};
|
||||
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||
use smithay::{
|
||||
@@ -36,8 +37,11 @@ use smithay::{
|
||||
};
|
||||
use tracing::field::Empty;
|
||||
|
||||
use crate::input::move_grab::MoveGrab;
|
||||
use crate::input::resize_grab::ResizeGrab;
|
||||
use crate::input::DOUBLE_CLICK_TIME;
|
||||
use crate::input::touch_move_grab::TouchMoveGrab;
|
||||
use crate::input::touch_resize_grab::TouchResizeGrab;
|
||||
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
use crate::niri::{PopupGrabState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
@@ -65,8 +69,94 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
|
||||
// FIXME
|
||||
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
|
||||
let wl_surface = surface.wl_surface();
|
||||
|
||||
let mut grab_start_data = None;
|
||||
|
||||
// See if this comes from a pointer grab.
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
pointer.with_grab(|grab_serial, grab| {
|
||||
if grab_serial == serial {
|
||||
let start_data = grab.start_data();
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
Some(PointerOrTouchStartData::Pointer(start_data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// See if this comes from a touch grab.
|
||||
if let Some(touch) = self.niri.seat.get_touch() {
|
||||
touch.with_grab(|grab_serial, grab| {
|
||||
if grab_serial == serial {
|
||||
let start_data = grab.start_data();
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
Some(PointerOrTouchStartData::Touch(start_data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let Some(start_data) = grab_start_data else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
|
||||
let output_pos = self
|
||||
.niri
|
||||
.global_space
|
||||
.output_geometry(&output)
|
||||
.unwrap()
|
||||
.loc
|
||||
.to_f64();
|
||||
|
||||
let pos_within_output = start_data.location() - output_pos;
|
||||
|
||||
if !self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_move_begin(window.clone(), &output, pos_within_output)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = MoveGrab::new(start_data, window);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
let grab = TouchMoveGrab::new(start_data, window);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
fn resize_request(
|
||||
@@ -76,24 +166,39 @@ impl XdgShellHandler for State {
|
||||
serial: Serial,
|
||||
edges: xdg_toplevel::ResizeEdge,
|
||||
) {
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
if !pointer.has_grab(serial) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(start_data) = pointer.grab_start_data() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((focus, _)) = &start_data.focus else {
|
||||
return;
|
||||
};
|
||||
|
||||
let wl_surface = surface.wl_surface();
|
||||
if !focus.id().same_client_as(&wl_surface.id()) {
|
||||
return;
|
||||
|
||||
let mut grab_start_data = None;
|
||||
|
||||
// See if this comes from a pointer grab.
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
if pointer.has_grab(serial) {
|
||||
if let Some(start_data) = pointer.grab_start_data() {
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if this comes from a touch grab.
|
||||
if let Some(touch) = self.niri.seat.get_touch() {
|
||||
if touch.has_grab(serial) {
|
||||
if let Some(start_data) = touch.grab_start_data() {
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(start_data) = grab_start_data else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
|
||||
return;
|
||||
};
|
||||
@@ -128,14 +233,25 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
|
||||
if !self.niri.layout.interactive_resize_begin(window, edges) {
|
||||
if !self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.pointer_grab_ongoing = true;
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = ResizeGrab::new(start_data, window);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
let grab = TouchResizeGrab::new(start_data, window);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(
|
||||
@@ -261,7 +377,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&surface) {
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -288,7 +404,7 @@ impl XdgShellHandler for State {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +448,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = mon
|
||||
@@ -416,7 +532,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = workspace_name
|
||||
@@ -552,7 +668,7 @@ impl XdgDecorationHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&toplevel) {
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -565,7 +681,7 @@ impl XdgDecorationHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&toplevel) {
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -620,18 +736,6 @@ impl XdgForeignHandler for State {
|
||||
}
|
||||
delegate_xdg_foreign!(State);
|
||||
|
||||
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
})
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
|
||||
let _span = tracy_client::span!("State::send_initial_configure");
|
||||
@@ -706,7 +810,7 @@ impl State {
|
||||
// mapped, it fetches the possibly changed parent's output again, and shows up there.
|
||||
let output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let mut width = None;
|
||||
@@ -759,7 +863,7 @@ impl State {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name: ws.and_then(|w| w.name.clone()),
|
||||
workspace_name: ws.and_then(|w| w.name().cloned()),
|
||||
};
|
||||
|
||||
toplevel.send_configure();
|
||||
@@ -788,16 +892,7 @@ impl State {
|
||||
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
||||
match popup {
|
||||
PopupKind::Xdg(ref popup) => {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgPopupSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
if !initial_configure_sent {
|
||||
if !popup.is_initial_configure_sent() {
|
||||
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
|
||||
{
|
||||
let scale = output.current_scale();
|
||||
|
||||
+331
-70
@@ -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_when_typing {
|
||||
return;
|
||||
}
|
||||
|
||||
// niri keeps this set only while actively using a tablet, which means the cursor position
|
||||
// is likely to change almost immediately, causing pointer_hidden to just flicker back and
|
||||
// forth.
|
||||
if self.niri.tablet_cursor_location.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.niri.pointer_hidden = true;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
pub fn handle_bind(&mut self, bind: Bind) {
|
||||
let Some(cooldown) = bind.cooldown else {
|
||||
self.do_action(bind.action, bind.allow_when_locked);
|
||||
@@ -452,6 +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();
|
||||
@@ -1240,12 +1320,17 @@ impl State {
|
||||
self.niri.tablet_cursor_location = None;
|
||||
|
||||
// Check if we have an active pointer constraint.
|
||||
//
|
||||
// FIXME: ideally this should use the pointer focus with up-to-date global location.
|
||||
let mut pointer_confined = None;
|
||||
if let Some(focus) = &self.niri.pointer_focus.surface {
|
||||
let pos_within_surface = pos - focus.1;
|
||||
if let Some(under) = &self.niri.pointer_contents.surface {
|
||||
// No need to check if the pointer focus surface matches, because here we're checking
|
||||
// for an already-active constraint, and the constraint is deactivated when the focused
|
||||
// surface changes.
|
||||
let pos_within_surface = pos - under.1;
|
||||
|
||||
let mut pointer_locked = false;
|
||||
with_pointer_constraint(&focus.0, &pointer, |constraint| {
|
||||
with_pointer_constraint(&under.0, &pointer, |constraint| {
|
||||
let Some(constraint) = constraint else { return };
|
||||
if !constraint.is_active() {
|
||||
return;
|
||||
@@ -1263,7 +1348,7 @@ impl State {
|
||||
pointer_locked = true;
|
||||
}
|
||||
PointerConstraint::Confined(confine) => {
|
||||
pointer_confined = Some((focus.clone(), confine.region().cloned()));
|
||||
pointer_confined = Some((under.clone(), confine.region().cloned()));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1272,7 +1357,7 @@ impl State {
|
||||
if pointer_locked {
|
||||
pointer.relative_motion(
|
||||
self,
|
||||
Some(focus.clone()),
|
||||
Some(under.clone()),
|
||||
&RelativeMotionEvent {
|
||||
delta: event.delta(),
|
||||
delta_unaccel: event.delta_unaccel(),
|
||||
@@ -1330,7 +1415,7 @@ impl State {
|
||||
self.niri.screenshot_ui.pointer_motion(point);
|
||||
}
|
||||
|
||||
let under = self.niri.surface_under_and_global_space(new_pos);
|
||||
let under = self.niri.contents_under(new_pos);
|
||||
|
||||
// Handle confined pointer.
|
||||
if let Some((focus_surface, region)) = pointer_confined {
|
||||
@@ -1368,10 +1453,7 @@ impl State {
|
||||
|
||||
self.niri.handle_focus_follows_mouse(&under);
|
||||
|
||||
// Activate a new confinement if necessary.
|
||||
self.niri.maybe_activate_pointer_constraint(new_pos, &under);
|
||||
|
||||
self.niri.pointer_focus.clone_from(&under);
|
||||
self.niri.pointer_contents.clone_from(&under);
|
||||
|
||||
pointer.motion(
|
||||
self,
|
||||
@@ -1395,6 +1477,9 @@ impl State {
|
||||
|
||||
pointer.frame(self);
|
||||
|
||||
// Activate a new confinement if necessary.
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
// Redraw to update the cursor position.
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -1429,12 +1514,11 @@ impl State {
|
||||
self.niri.screenshot_ui.pointer_motion(point);
|
||||
}
|
||||
|
||||
let under = self.niri.surface_under_and_global_space(pos);
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
self.niri.handle_focus_follows_mouse(&under);
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint(pos, &under);
|
||||
self.niri.pointer_focus.clone_from(&under);
|
||||
self.niri.pointer_contents.clone_from(&under);
|
||||
|
||||
pointer.motion(
|
||||
self,
|
||||
@@ -1448,6 +1532,8 @@ impl State {
|
||||
|
||||
pointer.frame(self);
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
// We moved the pointer, show it.
|
||||
self.niri.pointer_hidden = false;
|
||||
|
||||
@@ -1469,11 +1555,47 @@ impl State {
|
||||
let button_state = event.state();
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
// We received an event for the regular pointer, so show it now.
|
||||
self.niri.pointer_hidden = false;
|
||||
self.niri.tablet_cursor_location = None;
|
||||
|
||||
if let Some(mapped) = self.niri.window_under_cursor() {
|
||||
let window = mapped.window.clone();
|
||||
|
||||
// Check if we need to start an interactive move.
|
||||
if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mod_down = match self.backend.mod_key() {
|
||||
CompositorMod::Super => mods.logo,
|
||||
CompositorMod::Alt => mods.alt,
|
||||
};
|
||||
if mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let output = output.clone();
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
if self.niri.layout.interactive_move_begin(
|
||||
window.clone(),
|
||||
&output,
|
||||
pos_within_output,
|
||||
) {
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: event.button_code(),
|
||||
location,
|
||||
};
|
||||
let grab = MoveGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if we need to start an interactive resize.
|
||||
if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
|
||||
else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mod_down = match self.backend.mod_key() {
|
||||
CompositorMod::Super => mods.logo,
|
||||
@@ -1531,7 +1653,6 @@ impl State {
|
||||
};
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.pointer_grab_ongoing = true;
|
||||
self.niri.cursor_manager.set_cursor_image(
|
||||
CursorImageStatus::Named(edges.cursor_icon()),
|
||||
);
|
||||
@@ -1567,7 +1688,6 @@ impl State {
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.pointer_grab_ongoing = true;
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
@@ -1576,11 +1696,11 @@ impl State {
|
||||
}
|
||||
};
|
||||
|
||||
self.update_pointer_focus();
|
||||
self.update_pointer_contents();
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
let layer_focus = self.niri.pointer_focus.layer.clone();
|
||||
self.niri.focus_layer_surface_if_on_demand(layer_focus);
|
||||
let layer_under = self.niri.pointer_contents.layer.clone();
|
||||
self.niri.focus_layer_surface_if_on_demand(layer_under);
|
||||
}
|
||||
|
||||
if let Some(button) = event.button() {
|
||||
@@ -1761,14 +1881,23 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
let scroll_factor = match source {
|
||||
AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor.0,
|
||||
AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
let horizontal_amount = horizontal_amount.unwrap_or_else(|| {
|
||||
// Winit backend, discrete scrolling.
|
||||
horizontal_amount_v120.unwrap_or(0.0) / 120. * 15.
|
||||
});
|
||||
}) * scroll_factor;
|
||||
let vertical_amount = vertical_amount.unwrap_or_else(|| {
|
||||
// Winit backend, discrete scrolling.
|
||||
vertical_amount_v120.unwrap_or(0.0) / 120. * 15.
|
||||
});
|
||||
}) * scroll_factor;
|
||||
|
||||
let horizontal_amount_v120 = horizontal_amount_v120.map(|x| x * scroll_factor);
|
||||
let vertical_amount_v120 = vertical_amount_v120.map(|x| x * scroll_factor);
|
||||
|
||||
let mut frame = AxisFrame::new(event.time_msec()).source(source);
|
||||
if horizontal_amount != 0.0 {
|
||||
@@ -1797,7 +1926,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
self.update_pointer_focus();
|
||||
self.update_pointer_contents();
|
||||
|
||||
let pointer = &self.niri.seat.get_pointer().unwrap();
|
||||
pointer.axis(self, frame);
|
||||
@@ -1812,7 +1941,7 @@ impl State {
|
||||
return;
|
||||
};
|
||||
|
||||
let under = self.niri.surface_under_and_global_space(pos);
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
let tablet_seat = self.niri.seat.tablet_seat();
|
||||
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
|
||||
@@ -1864,7 +1993,7 @@ impl State {
|
||||
tool.tip_down(serial, event.time_msec());
|
||||
|
||||
if let Some(pos) = self.niri.tablet_cursor_location {
|
||||
let under = self.niri.surface_under_and_global_space(pos);
|
||||
let under = self.niri.contents_under(pos);
|
||||
if let Some(window) = under.window {
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
@@ -1894,7 +2023,7 @@ impl State {
|
||||
return;
|
||||
};
|
||||
|
||||
let under = self.niri.surface_under_and_global_space(pos);
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
let tablet_seat = self.niri.seat.tablet_seat();
|
||||
let display_handle = self.niri.display_handle.clone();
|
||||
@@ -1926,6 +2055,7 @@ impl State {
|
||||
self.move_cursor(pos);
|
||||
}
|
||||
|
||||
self.niri.pointer_hidden = false;
|
||||
self.niri.tablet_cursor_location = None;
|
||||
}
|
||||
}
|
||||
@@ -1959,7 +2089,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2050,7 +2180,7 @@ impl State {
|
||||
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2093,7 +2223,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2111,7 +2241,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2128,7 +2258,7 @@ impl State {
|
||||
fn on_gesture_pinch_update<I: InputBackend>(&mut self, event: I::GesturePinchUpdateEvent) {
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2147,7 +2277,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2165,7 +2295,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2183,7 +2313,7 @@ impl State {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
|
||||
if self.update_pointer_focus() {
|
||||
if self.update_pointer_contents() {
|
||||
pointer.frame(self);
|
||||
}
|
||||
|
||||
@@ -2222,7 +2352,7 @@ impl State {
|
||||
return;
|
||||
};
|
||||
|
||||
let under = self.niri.surface_under_and_global_space(touch_location);
|
||||
let under = self.niri.contents_under(touch_location);
|
||||
|
||||
if !handle.is_grabbed() {
|
||||
if let Some(window) = under.window {
|
||||
@@ -2275,7 +2405,7 @@ impl State {
|
||||
let Some(touch_location) = self.compute_touch_location(&evt) else {
|
||||
return;
|
||||
};
|
||||
let under = self.niri.surface_under_and_global_space(touch_location);
|
||||
let under = self.niri.contents_under(touch_location);
|
||||
handle.motion(
|
||||
self,
|
||||
under.surface,
|
||||
@@ -2298,6 +2428,28 @@ impl State {
|
||||
};
|
||||
handle.cancel(self);
|
||||
}
|
||||
|
||||
fn on_switch_toggle<I: InputBackend>(&mut self, evt: I::SwitchToggleEvent) {
|
||||
let Some(switch) = evt.switch() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if switch == Switch::Lid {
|
||||
let is_closed = evt.state() == SwitchState::On;
|
||||
debug!("lid switch {}", if is_closed { "closed" } else { "opened" });
|
||||
self.niri.is_lid_closed = is_closed;
|
||||
self.backend.on_output_config_changed(&mut self.niri);
|
||||
}
|
||||
|
||||
let action = {
|
||||
let bindings = &self.niri.config.borrow().switch_events;
|
||||
find_configured_switch_action(bindings, switch, evt.state())
|
||||
};
|
||||
|
||||
if let Some(action) = action {
|
||||
self.do_action(action, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
@@ -2305,10 +2457,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 +2601,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 +2699,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 +2720,7 @@ fn allowed_when_locked(action: &Action) -> bool {
|
||||
| Action::ChangeVt(_)
|
||||
| Action::Suspend
|
||||
| Action::PowerOffMonitors
|
||||
| Action::PowerOnMonitors
|
||||
| Action::SwitchLayout(_)
|
||||
)
|
||||
}
|
||||
@@ -2544,7 +2728,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 +2764,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 +2832,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 +2905,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 +3015,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 +3032,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,
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
is_moving: bool,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
is_moving: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for MoveGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if self.is_moving {
|
||||
data.niri.layout.view_offset_gesture_update(
|
||||
-event_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
}
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// MouseButton::Middle
|
||||
if event.button == 0x112 {
|
||||
if event.state == ButtonState::Pressed {
|
||||
let output = data
|
||||
.niri
|
||||
.output_under(handle.current_location())
|
||||
.map(|(output, _)| output)
|
||||
.cloned();
|
||||
// FIXME: workspace switch gesture.
|
||||
if let Some(output) = output {
|
||||
self.is_moving = true;
|
||||
data.niri.layout.view_offset_gesture_begin(&output, false);
|
||||
}
|
||||
} else if event.state == ButtonState::Released {
|
||||
self.is_moving = false;
|
||||
data.niri.layout.view_offset_gesture_end(false, None);
|
||||
}
|
||||
}
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ impl ResizeGrab {
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
|
||||
@@ -50,7 +50,6 @@ impl SpatialMovementGrab {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchResizeGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchResizeGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchResizeGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
let delta = event.location - self.start_data.location;
|
||||
let ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
+25
-40
@@ -16,17 +16,14 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use smithay::input::keyboard::XkbContextHandler;
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::rustix::fs::unlink;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::version;
|
||||
use crate::utils::{version, with_toplevel_role};
|
||||
use crate::window::Mapped;
|
||||
|
||||
// If an event stream client fails to read events fast enough that we accumulate more than this
|
||||
@@ -362,22 +359,12 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result
|
||||
}
|
||||
|
||||
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
}
|
||||
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -385,10 +372,13 @@ impl State {
|
||||
pub fn ipc_keyboard_layouts_changed(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
|
||||
let layouts = context.keymap().layouts();
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
let layouts = xkb.layouts();
|
||||
KeyboardLayouts {
|
||||
names: layouts.map(str::to_owned).collect(),
|
||||
current_idx: context.active_layout().0 as u8,
|
||||
names: layouts
|
||||
.map(|layout| xkb.layout_name(layout).to_owned())
|
||||
.collect(),
|
||||
current_idx: xkb.active_layout().0 as u8,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -406,7 +396,10 @@ impl State {
|
||||
|
||||
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let idx = keyboard.with_xkb_state(self, |context| context.active_layout().0 as u8);
|
||||
let idx = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
xkb.active_layout().0 as u8
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
@@ -459,7 +452,7 @@ impl State {
|
||||
// Check for any changes that we can't signal as individual events.
|
||||
let output_name = mon.map(|mon| mon.output_name());
|
||||
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|
||||
|| ipc_ws.name != ws.name
|
||||
|| ipc_ws.name.as_ref() != ws.name()
|
||||
|| ipc_ws.output.as_ref() != output_name
|
||||
{
|
||||
need_workspaces_changed = true;
|
||||
@@ -482,7 +475,7 @@ impl State {
|
||||
}
|
||||
|
||||
// Check if this workspace became active.
|
||||
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx);
|
||||
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
|
||||
if is_active && !ipc_ws.is_active {
|
||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||
}
|
||||
@@ -503,9 +496,9 @@ impl State {
|
||||
Workspace {
|
||||
id,
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name.clone(),
|
||||
name: ws.name().cloned(),
|
||||
output: mon.map(|mon| mon.output_name().clone()),
|
||||
is_active: mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx),
|
||||
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
|
||||
is_focused: Some(id) == focused_ws_id,
|
||||
active_window_id: ws.active_window().map(|win| win.id().get()),
|
||||
}
|
||||
@@ -546,28 +539,20 @@ impl State {
|
||||
}
|
||||
|
||||
let Some(ipc_win) = state.windows.get(&id) else {
|
||||
let window = make_ipc_window(mapped, Some(ws_id));
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace_id = Some(ws_id.get());
|
||||
let workspace_id = ws_id.map(|id| id.get());
|
||||
let mut changed = ipc_win.workspace_id != workspace_id;
|
||||
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
changed |= with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
||||
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
||||
});
|
||||
|
||||
if changed {
|
||||
let window = make_ipc_window(mapped, Some(ws_id));
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use niri_config::{CornerRadius, FloatOrInt};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertHintElement {
|
||||
inner: FocusRing,
|
||||
}
|
||||
|
||||
pub type InsertHintRenderElement = FocusRingRenderElement;
|
||||
|
||||
impl InsertHintElement {
|
||||
pub fn new(config: niri_config::InsertHint) -> Self {
|
||||
Self {
|
||||
inner: FocusRing::new(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::InsertHint) {
|
||||
self.inner.update_config(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.inner.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
}
|
||||
}
|
||||
+1216
-166
File diff suppressed because it is too large
Load Diff
+210
-216
@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::utils::{
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Rectangle};
|
||||
|
||||
use super::tile::Tile;
|
||||
use super::workspace::{
|
||||
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
|
||||
WorkspaceRenderElement,
|
||||
@@ -20,7 +21,7 @@ use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{output_size, to_physical_precise_round, ResizeEdge};
|
||||
use crate::utils::{output_size, round_logical_in_physical, ResizeEdge};
|
||||
|
||||
/// Amount of touchpad movement to scroll the height of one workspace.
|
||||
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
|
||||
@@ -33,19 +34,19 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
||||
#[derive(Debug)]
|
||||
pub struct Monitor<W: LayoutElement> {
|
||||
/// Output for this monitor.
|
||||
pub output: Output,
|
||||
pub(super) output: Output,
|
||||
/// Cached name of the output.
|
||||
output_name: String,
|
||||
// Must always contain at least one.
|
||||
pub workspaces: Vec<Workspace<W>>,
|
||||
pub(super) workspaces: Vec<Workspace<W>>,
|
||||
/// Index of the currently active workspace.
|
||||
pub active_workspace_idx: usize,
|
||||
pub(super) active_workspace_idx: usize,
|
||||
/// ID of the previously active workspace.
|
||||
pub previous_workspace_id: Option<WorkspaceId>,
|
||||
pub(super) previous_workspace_id: Option<WorkspaceId>,
|
||||
/// In-progress switch between workspaces.
|
||||
pub workspace_switch: Option<WorkspaceSwitch>,
|
||||
pub(super) workspace_switch: Option<WorkspaceSwitch>,
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
pub(super) options: Rc<Options>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -59,7 +60,7 @@ pub struct WorkspaceSwitchGesture {
|
||||
/// Index of the workspace where the gesture was started.
|
||||
center_idx: usize,
|
||||
/// Current, fractional workspace index.
|
||||
pub current_idx: f64,
|
||||
pub(super) current_idx: f64,
|
||||
tracker: SwipeTracker,
|
||||
/// Whether the gesture is controlled by the touchpad.
|
||||
is_touchpad: bool,
|
||||
@@ -105,10 +106,18 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output(&self) -> &Output {
|
||||
&self.output
|
||||
}
|
||||
|
||||
pub fn output_name(&self) -> &String {
|
||||
&self.output_name
|
||||
}
|
||||
|
||||
pub fn active_workspace_idx(&self) -> usize {
|
||||
self.active_workspace_idx
|
||||
}
|
||||
|
||||
pub fn active_workspace_ref(&self) -> &Workspace<W> {
|
||||
&self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
@@ -175,7 +184,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_window(window, activate, width, is_full_width);
|
||||
workspace.add_window(None, window, activate, width, is_full_width);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
@@ -209,12 +218,15 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
// Since we're adding window right of something, the workspace isn't empty, and therefore
|
||||
// cannot be the last one, so we never need to insert a new empty workspace.
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_column(column, activate);
|
||||
workspace.add_column(None, column, activate, None);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
@@ -230,6 +242,56 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tile(
|
||||
&mut self,
|
||||
workspace_idx: usize,
|
||||
column_idx: Option<usize>,
|
||||
tile: Tile<W>,
|
||||
activate: bool,
|
||||
width: ColumnWidth,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
if workspace_idx == self.workspaces.len() - 1 {
|
||||
// Insert a new empty workspace.
|
||||
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||
self.workspaces.push(ws);
|
||||
}
|
||||
|
||||
if activate {
|
||||
self.activate_workspace(workspace_idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tile_to_column(
|
||||
&mut self,
|
||||
workspace_idx: usize,
|
||||
column_idx: usize,
|
||||
tile_idx: Option<usize>,
|
||||
tile: Tile<W>,
|
||||
activate: bool,
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_tile_to_column(column_idx, tile_idx, tile, activate);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
// Since we're adding window to an existing column, the workspace isn't empty, and
|
||||
// therefore cannot be the last one, so we never need to insert a new empty workspace.
|
||||
|
||||
if activate {
|
||||
self.activate_workspace(workspace_idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_up_workspaces(&mut self) {
|
||||
assert!(self.workspace_switch.is_none());
|
||||
|
||||
@@ -314,14 +376,6 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_left(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_left();
|
||||
}
|
||||
|
||||
pub fn consume_or_expel_window_right(&mut self) {
|
||||
self.active_workspace().consume_or_expel_window_right();
|
||||
}
|
||||
|
||||
pub fn focus_left(&mut self) {
|
||||
self.active_workspace().focus_left();
|
||||
}
|
||||
@@ -403,7 +457,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
|
||||
let new_idx = curr_idx.saturating_sub(1);
|
||||
if curr_idx == new_idx {
|
||||
self.focus_left();
|
||||
self.focus_right();
|
||||
} else {
|
||||
workspace.focus_up();
|
||||
}
|
||||
@@ -455,18 +509,20 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
|
||||
let column = &workspace.columns[workspace.active_column_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
)
|
||||
.into_window();
|
||||
let removed = workspace.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
);
|
||||
|
||||
self.add_window(new_idx, window, true, width, is_full_width);
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
true,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn move_to_workspace_down(&mut self) {
|
||||
@@ -483,18 +539,20 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
|
||||
let column = &workspace.columns[workspace.active_column_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
)
|
||||
.into_window();
|
||||
let removed = workspace.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
);
|
||||
|
||||
self.add_window(new_idx, window, true, width, is_full_width);
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
true,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
|
||||
@@ -531,17 +589,19 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||
let column = &workspace.columns[col_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let activate = source_workspace_idx == self.active_workspace_idx
|
||||
&& col_idx == workspace.active_column_idx
|
||||
&& tile_idx == column.active_tile_idx;
|
||||
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None)
|
||||
.into_window();
|
||||
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
|
||||
|
||||
self.add_window(new_idx, window, activate, width, is_full_width);
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
activate,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
|
||||
if self.workspace_switch.is_none() {
|
||||
self.clean_up_workspaces();
|
||||
@@ -561,7 +621,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
}
|
||||
|
||||
@@ -578,7 +638,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
}
|
||||
|
||||
@@ -595,7 +655,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
}
|
||||
|
||||
@@ -673,7 +733,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
pub(super) fn are_animations_ongoing(&self) -> bool {
|
||||
self.workspace_switch
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.is_animation())
|
||||
@@ -811,90 +871,75 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
Some(rect)
|
||||
}
|
||||
|
||||
pub fn workspaces_with_render_positions(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
|
||||
let mut first = None;
|
||||
let mut second = None;
|
||||
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
if after_idx >= 0. && before_idx < self.workspaces.len() as f64 {
|
||||
let scale = self.output.current_scale().fractional_scale();
|
||||
let size = output_size(&self.output);
|
||||
let offset =
|
||||
round_logical_in_physical(scale, (render_idx - before_idx) * size.h);
|
||||
|
||||
// Ceil the height in physical pixels.
|
||||
let height = (size.h * scale).ceil() / scale;
|
||||
|
||||
if before_idx >= 0. {
|
||||
let before_idx = before_idx as usize;
|
||||
let before_offset = Point::from((0., -offset));
|
||||
first = Some((&self.workspaces[before_idx], before_offset));
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
if after_idx < self.workspaces.len() {
|
||||
let after_offset = Point::from((0., -offset + height));
|
||||
second = Some((&self.workspaces[after_idx], after_offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
first = Some((
|
||||
&self.workspaces[self.active_workspace_idx],
|
||||
Point::from((0., 0.)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
first.into_iter().chain(second)
|
||||
}
|
||||
|
||||
pub fn workspace_under(
|
||||
&self,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<(&Workspace<W>, Point<f64, Logical>)> {
|
||||
let size = output_size(&self.output);
|
||||
let (ws, bounds) = self
|
||||
.workspaces_with_render_positions()
|
||||
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
|
||||
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
|
||||
Some((ws, bounds.loc))
|
||||
}
|
||||
|
||||
pub fn window_under(
|
||||
&self,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<(&W, Option<Point<f64, Logical>>)> {
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let size = output_size(&self.output).to_f64();
|
||||
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
|
||||
let (idx, ws_offset) = if pos_within_output.y < size.h - offset {
|
||||
if before_idx < 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
(before_idx as usize, Point::from((0., offset)))
|
||||
} else {
|
||||
if after_idx >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(after_idx, Point::from((0., -size.h + offset)))
|
||||
};
|
||||
|
||||
let ws = &self.workspaces[idx];
|
||||
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset)?;
|
||||
Some((win, win_pos.map(|p| p - ws_offset)))
|
||||
}
|
||||
None => {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.window_under(pos_within_output)
|
||||
}
|
||||
}
|
||||
let (ws, offset) = self.workspace_under(pos_within_output)?;
|
||||
let (win, win_pos) = ws.window_under(pos_within_output - offset)?;
|
||||
Some((win, win_pos.map(|p| p + offset)))
|
||||
}
|
||||
|
||||
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let size = output_size(&self.output);
|
||||
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
|
||||
let (idx, ws_offset) = if pos_within_output.y < size.h - offset {
|
||||
if before_idx < 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
(before_idx as usize, Point::from((0., offset)))
|
||||
} else {
|
||||
if after_idx >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(after_idx, Point::from((0., -size.h + offset)))
|
||||
};
|
||||
|
||||
let ws = &self.workspaces[idx];
|
||||
ws.resize_edges_under(pos_within_output + ws_offset)
|
||||
}
|
||||
None => {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.resize_edges_under(pos_within_output)
|
||||
}
|
||||
}
|
||||
let (ws, offset) = self.workspace_under(pos_within_output)?;
|
||||
ws.resize_edges_under(pos_within_output - offset)
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
@@ -907,103 +952,52 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
ws.render_above_top_layer()
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
pub fn render_elements<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
target: RenderTarget,
|
||||
) -> Vec<MonitorRenderElement<R>> {
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> + '_ {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
|
||||
let scale = self.output.current_scale().fractional_scale();
|
||||
let size = output_size(&self.output);
|
||||
// Ceil the height in physical pixels.
|
||||
let height = (size.h * scale).ceil() as i32;
|
||||
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
// Crop the elements to prevent them overflowing, currently visible during a workspace
|
||||
// switch.
|
||||
//
|
||||
// HACK: crop to infinite bounds at least horizontally where we
|
||||
// know there's no workspace joining or monitor bounds, otherwise
|
||||
// it will cut pixel shaders and mess up the coordinate space.
|
||||
// There's also a damage tracking bug which causes glitched
|
||||
// rendering for maximized GTK windows.
|
||||
//
|
||||
// FIXME: use proper bounds after fixing the Crop element.
|
||||
let crop_bounds = if self.workspace_switch.is_some() {
|
||||
Rectangle::from_loc_and_size((-i32::MAX / 2, 0), (i32::MAX, height))
|
||||
} else {
|
||||
Rectangle::from_loc_and_size((-i32::MAX / 2, -i32::MAX / 2), (i32::MAX, i32::MAX))
|
||||
};
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
let after = if after_idx < self.workspaces.len() {
|
||||
let after = self.workspaces[after_idx].render_elements(renderer, target);
|
||||
let after = after.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
scale,
|
||||
// HACK: crop to infinite bounds for all sides except the side
|
||||
// where the workspaces join,
|
||||
// otherwise it will cut pixel shaders and mess up
|
||||
// the coordinate space.
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, 0),
|
||||
(i32::MAX / 2, i32::MAX / 2),
|
||||
),
|
||||
)?,
|
||||
Point::from((0., -offset + size.h)).to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
|
||||
if before_idx < 0. {
|
||||
return after.collect();
|
||||
}
|
||||
|
||||
Some(after)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let before_idx = before_idx as usize;
|
||||
let before = self.workspaces[before_idx].render_elements(renderer, target);
|
||||
let before = before.into_iter().filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
scale,
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX / 2, to_physical_precise_round(scale, size.h)),
|
||||
),
|
||||
)?,
|
||||
Point::from((0., -offset)).to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
before.chain(after.into_iter().flatten()).collect()
|
||||
}
|
||||
None => {
|
||||
let elements =
|
||||
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
|
||||
elements
|
||||
self.workspaces_with_render_positions()
|
||||
.flat_map(move |(ws, offset)| {
|
||||
ws.render_elements(renderer, target)
|
||||
.into_iter()
|
||||
.filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
scale,
|
||||
// HACK: set infinite crop bounds due to a damage tracking bug
|
||||
// which causes glitched rendering for maximized GTK windows.
|
||||
// FIXME: use proper bounds after fixing the Crop element.
|
||||
Rectangle::from_loc_and_size(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX, i32::MAX),
|
||||
),
|
||||
// Rectangle::from_loc_and_size((0, 0), size),
|
||||
)?,
|
||||
(0, 0),
|
||||
Relocate::Relative,
|
||||
))
|
||||
.filter_map(move |elem| {
|
||||
CropRenderElement::from_element(elem, scale, crop_bounds)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
.map(move |elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_positions() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
offset.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
|
||||
|
||||
+14
-3
@@ -64,6 +64,9 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The animation of a tile visually moving vertically.
|
||||
move_y_animation: Option<MoveAnimation>,
|
||||
|
||||
/// Offset during the initial interactive move rubberband.
|
||||
pub(super) interactive_move_offset: Point<f64, Logical>,
|
||||
|
||||
/// Snapshot of the last render for use in the close animation.
|
||||
unmap_snapshot: Option<TileRenderSnapshot>,
|
||||
|
||||
@@ -74,7 +77,7 @@ pub struct Tile<W: LayoutElement> {
|
||||
scale: f64,
|
||||
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
pub(super) options: Rc<Options>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
@@ -90,7 +93,7 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
type TileRenderSnapshot =
|
||||
pub type TileRenderSnapshot =
|
||||
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -123,6 +126,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
resize_animation: None,
|
||||
move_x_animation: None,
|
||||
move_y_animation: None,
|
||||
interactive_move_offset: Point::from((0., 0.)),
|
||||
unmap_snapshot: None,
|
||||
rounded_corner_damage: Default::default(),
|
||||
scale,
|
||||
@@ -305,6 +309,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
offset.y += move_.from * move_.anim.value();
|
||||
}
|
||||
|
||||
offset += self.interactive_move_offset;
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
@@ -364,6 +370,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop_move_animations(&mut self) {
|
||||
self.move_x_animation = None;
|
||||
self.move_y_animation = None;
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &W {
|
||||
&self.window
|
||||
}
|
||||
@@ -381,7 +392,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
||||
fn effective_border_width(&self) -> Option<f64> {
|
||||
pub fn effective_border_width(&self) -> Option<f64> {
|
||||
if self.is_fullscreen {
|
||||
return None;
|
||||
}
|
||||
|
||||
+606
-425
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,11 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
|
||||
|
||||
#[cfg(feature = "profile-with-tracy-allocations")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
if env::var_os("RUST_BACKTRACE").is_none() {
|
||||
|
||||
+225
-104
@@ -17,6 +17,7 @@ use niri_config::{
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::Keycode;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
@@ -43,7 +44,7 @@ use smithay::desktop::{
|
||||
layer_map_for_output, LayerSurface, PopupGrab, PopupManager, PopupUngrabStrategy, Space,
|
||||
Window, WindowSurfaceType,
|
||||
};
|
||||
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler};
|
||||
use smithay::input::keyboard::Layout as KeyboardLayout;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
|
||||
use smithay::input::{Seat, SeatState};
|
||||
use smithay::output::{self, Output, OutputModeSource, PhysicalProperties, Subpixel};
|
||||
@@ -109,12 +110,13 @@ use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
|
||||
use crate::frame_clock::FrameClock;
|
||||
use crate::handlers::configure_lock_surface;
|
||||
use crate::handlers::{configure_lock_surface, XDG_ACTIVATION_TOKEN_TIMEOUT};
|
||||
use crate::input::scroll_tracker::ScrollTracker;
|
||||
use crate::input::{
|
||||
apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData,
|
||||
};
|
||||
use crate::ipc::server::IpcServer;
|
||||
use crate::layout::tile::TileRenderElement;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement};
|
||||
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
|
||||
@@ -205,6 +207,12 @@ pub struct Niri {
|
||||
// When false, we're idling with monitors powered off.
|
||||
pub monitors_active: bool,
|
||||
|
||||
/// Whether the laptop lid is closed.
|
||||
///
|
||||
/// Libinput guarantees that the lid switch starts in open state, and if it was closed during
|
||||
/// startup, libinput will immediately send a closed event.
|
||||
pub is_lid_closed: bool,
|
||||
|
||||
pub devices: HashSet<input::Device>,
|
||||
pub tablets: HashMap<input::Device, TabletData>,
|
||||
pub touch: HashSet<input::Device>,
|
||||
@@ -248,7 +256,7 @@ pub struct Niri {
|
||||
|
||||
pub seat: Seat<State>,
|
||||
/// Scancodes of the keys to suppress.
|
||||
pub suppressed_keys: HashSet<u32>,
|
||||
pub suppressed_keys: HashSet<Keycode>,
|
||||
pub bind_cooldown_timers: HashMap<Key, RegistrationToken>,
|
||||
pub bind_repeat_timer: Option<RegistrationToken>,
|
||||
pub keyboard_focus: KeyboardFocus,
|
||||
@@ -259,15 +267,27 @@ pub struct Niri {
|
||||
pub cursor_manager: CursorManager,
|
||||
pub cursor_texture_cache: CursorTextureCache,
|
||||
pub cursor_shape_manager_state: CursorShapeManagerState,
|
||||
pub dnd_icon: Option<WlSurface>,
|
||||
pub pointer_focus: PointerFocus,
|
||||
pub dnd_icon: Option<DndIcon>,
|
||||
/// Contents under pointer.
|
||||
///
|
||||
/// Periodically updated: on motion and other events and in the loop callback. If you require
|
||||
/// the real up-to-date contents somewhere, it's better to recompute on the spot.
|
||||
///
|
||||
/// This is not pointer focus. I.e. during a click grab, the pointer focus remains on the
|
||||
/// client with the grab, but this field will keep updating to the latest contents as if no
|
||||
/// grab was active.
|
||||
///
|
||||
/// This is primarily useful for emitting pointer motion events for surfaces that move
|
||||
/// underneath the cursor on their own (i.e. when the tiling layout moves). In this case, not
|
||||
/// taking grabs into account is expected, because we pass the information to pointer.motion()
|
||||
/// which passes it down through grabs, which decide what to do with it as they see fit.
|
||||
pub pointer_contents: PointContents,
|
||||
/// Whether the pointer is hidden, for example due to a previous touch input.
|
||||
///
|
||||
/// When this happens, the pointer also loses any focus. This is so that touch can prevent
|
||||
/// various tooltips from sticking around.
|
||||
pub pointer_hidden: bool,
|
||||
// FIXME: this should be able to be removed once PointerFocus takes grabs into account.
|
||||
pub pointer_grab_ongoing: bool,
|
||||
pub pointer_inactivity_timer: Option<RegistrationToken>,
|
||||
pub tablet_cursor_location: Option<Point<f64, Logical>>,
|
||||
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
|
||||
pub vertical_wheel_tracker: ScrollTracker,
|
||||
@@ -304,6 +324,12 @@ pub struct Niri {
|
||||
pub mapped_cast_output: HashMap<Window, Output>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DndIcon {
|
||||
pub surface: WlSurface,
|
||||
pub offset: Point<i32, Logical>,
|
||||
}
|
||||
|
||||
pub struct OutputState {
|
||||
pub global: GlobalId,
|
||||
pub frame_clock: FrameClock,
|
||||
@@ -377,10 +403,10 @@ pub enum KeyboardFocus {
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
pub struct PointerFocus {
|
||||
// Output under pointer.
|
||||
pub struct PointContents {
|
||||
// Output under point.
|
||||
pub output: Option<Output>,
|
||||
// Surface under pointer and its location in global coordinate space.
|
||||
// Surface under point and its location in the global coordinate space.
|
||||
pub surface: Option<(WlSurface, Point<f64, Logical>)>,
|
||||
// If surface belongs to a window, this is that window.
|
||||
pub window: Option<Window>,
|
||||
@@ -533,15 +559,18 @@ impl State {
|
||||
self.notify_blocker_cleared();
|
||||
|
||||
// These should be called periodically, before flushing the clients.
|
||||
self.niri.layout.refresh();
|
||||
self.niri.cursor_manager.check_cursor_image_surface_alive();
|
||||
self.niri.refresh_pointer_outputs();
|
||||
self.niri.popups.cleanup();
|
||||
self.niri.global_space.refresh();
|
||||
self.niri.refresh_idle_inhibit();
|
||||
self.refresh_popup_grab();
|
||||
self.update_keyboard_focus();
|
||||
self.refresh_pointer_focus();
|
||||
|
||||
// Needs to be called after updating the keyboard focus.
|
||||
self.niri.refresh_layout();
|
||||
|
||||
self.niri.cursor_manager.check_cursor_image_surface_alive();
|
||||
self.niri.refresh_pointer_outputs();
|
||||
self.niri.global_space.refresh();
|
||||
self.niri.refresh_idle_inhibit();
|
||||
self.refresh_pointer_contents();
|
||||
foreign_toplevel::refresh(self);
|
||||
self.niri.refresh_window_rules();
|
||||
self.refresh_ipc_outputs();
|
||||
@@ -562,10 +591,8 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
|
||||
let under = self.niri.surface_under_and_global_space(location);
|
||||
self.niri
|
||||
.maybe_activate_pointer_constraint(location, &under);
|
||||
self.niri.pointer_focus.clone_from(&under);
|
||||
let under = self.niri.contents_under(location);
|
||||
self.niri.pointer_contents.clone_from(&under);
|
||||
|
||||
let pointer = &self.niri.seat.get_pointer().unwrap();
|
||||
pointer.motion(
|
||||
@@ -579,8 +606,9 @@ impl State {
|
||||
);
|
||||
pointer.frame(self);
|
||||
|
||||
// We moved the pointer, show it.
|
||||
self.niri.pointer_hidden = false;
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
// We do not show the pointer on programmatic or keyboard movement.
|
||||
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -634,14 +662,13 @@ impl State {
|
||||
let Some(output) = self.niri.layout.active_output() else {
|
||||
return false;
|
||||
};
|
||||
let output = output.clone();
|
||||
let monitor = self.niri.layout.monitor_for_output(&output).unwrap();
|
||||
let monitor = self.niri.layout.monitor_for_output(output).unwrap();
|
||||
|
||||
let mut rv = false;
|
||||
let rect = monitor.active_tile_visual_rectangle();
|
||||
|
||||
if let Some(rect) = rect {
|
||||
let output_geo = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut rect = rect;
|
||||
rect.loc += output_geo.loc.to_f64();
|
||||
rv = self.move_cursor_to_rect(rect, mode);
|
||||
@@ -666,8 +693,8 @@ impl State {
|
||||
self.move_cursor_to_focused_tile(CenterCoords::Both)
|
||||
}
|
||||
|
||||
pub fn refresh_pointer_focus(&mut self) {
|
||||
let _span = tracy_client::span!("Niri::refresh_pointer_focus");
|
||||
pub fn refresh_pointer_contents(&mut self) {
|
||||
let _span = tracy_client::span!("Niri::refresh_pointer_contents");
|
||||
|
||||
let pointer = &self.niri.seat.get_pointer().unwrap();
|
||||
let location = pointer.current_location();
|
||||
@@ -682,36 +709,37 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.update_pointer_focus() {
|
||||
if !self.update_pointer_contents() {
|
||||
return;
|
||||
}
|
||||
|
||||
pointer.frame(self);
|
||||
|
||||
// Pointer motion from a surface to nothing triggers a cursor change to default, which
|
||||
// means we may need to redraw.
|
||||
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
pub fn update_pointer_focus(&mut self) -> bool {
|
||||
let _span = tracy_client::span!("Niri::update_pointer_focus");
|
||||
pub fn update_pointer_contents(&mut self) -> bool {
|
||||
let _span = tracy_client::span!("Niri::update_pointer_contents");
|
||||
|
||||
let pointer = &self.niri.seat.get_pointer().unwrap();
|
||||
let location = pointer.current_location();
|
||||
let under = if self.niri.pointer_hidden {
|
||||
PointerFocus::default()
|
||||
PointContents::default()
|
||||
} else {
|
||||
self.niri.surface_under_and_global_space(location)
|
||||
self.niri.contents_under(location)
|
||||
};
|
||||
|
||||
// We're not changing the global cursor location here, so if the focus did not change, then
|
||||
// nothing changed.
|
||||
if self.niri.pointer_focus == under {
|
||||
// We're not changing the global cursor location here, so if the contents did not change,
|
||||
// then nothing changed.
|
||||
if self.niri.pointer_contents == under {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.niri
|
||||
.maybe_activate_pointer_constraint(location, &under);
|
||||
|
||||
self.niri.pointer_focus.clone_from(&under);
|
||||
self.niri.pointer_contents.clone_from(&under);
|
||||
|
||||
pointer.motion(
|
||||
self,
|
||||
@@ -723,6 +751,8 @@ impl State {
|
||||
},
|
||||
);
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -893,8 +923,10 @@ impl State {
|
||||
}
|
||||
|
||||
if self.niri.config.borrow().input.keyboard.track_layout == TrackLayout::Window {
|
||||
let current_layout =
|
||||
keyboard.with_xkb_state(self, |context| context.active_layout());
|
||||
let current_layout = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
xkb.active_layout()
|
||||
});
|
||||
|
||||
let mut new_layout = current_layout;
|
||||
// Store the currently active layout for the surface.
|
||||
@@ -982,6 +1014,7 @@ impl State {
|
||||
let mut window_rules_changed = false;
|
||||
let mut debug_config_changed = false;
|
||||
let mut shaders_changed = false;
|
||||
let mut cursor_inactivity_timeout_changed = false;
|
||||
let mut old_config = self.niri.config.borrow_mut();
|
||||
|
||||
// Reload the cursor.
|
||||
@@ -1068,8 +1101,18 @@ impl State {
|
||||
shaders_changed = true;
|
||||
}
|
||||
|
||||
if config.cursor.hide_after_inactive_ms != old_config.cursor.hide_after_inactive_ms {
|
||||
cursor_inactivity_timeout_changed = true;
|
||||
}
|
||||
|
||||
if config.debug != old_config.debug {
|
||||
debug_config_changed = true;
|
||||
|
||||
if config.debug.keep_laptop_panel_on_when_lid_is_closed
|
||||
!= old_config.debug.keep_laptop_panel_on_when_lid_is_closed
|
||||
{
|
||||
output_config_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
*old_config = config;
|
||||
@@ -1114,6 +1157,10 @@ impl State {
|
||||
self.niri.layout.update_shaders();
|
||||
}
|
||||
|
||||
if cursor_inactivity_timeout_changed {
|
||||
self.niri.reset_pointer_inactivity_timer();
|
||||
}
|
||||
|
||||
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
|
||||
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
|
||||
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
|
||||
@@ -1520,7 +1567,7 @@ impl State {
|
||||
to_introspect: &async_channel::Sender<NiriToIntrospect>,
|
||||
msg: IntrospectToNiri,
|
||||
) {
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
use crate::utils::with_toplevel_role;
|
||||
|
||||
let IntrospectToNiri::GetWindows = msg;
|
||||
let _span = tracy_client::span!("GetWindows");
|
||||
@@ -1528,21 +1575,8 @@ impl State {
|
||||
let mut windows = HashMap::new();
|
||||
|
||||
self.niri.layout.with_windows(|mapped, _, _| {
|
||||
let wl_surface = mapped
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("no X11 support")
|
||||
.wl_surface();
|
||||
|
||||
let id = mapped.id().get();
|
||||
let props = with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
let props = with_toplevel_role(mapped.toplevel(), |role| {
|
||||
gnome_shell_introspect::WindowProperties {
|
||||
title: role.title.clone().unwrap_or_default(),
|
||||
app_id: role.app_id.clone().unwrap_or_default(),
|
||||
@@ -1672,6 +1706,18 @@ impl Niri {
|
||||
is_tty && !client.get_data::<ClientState>().unwrap().restricted
|
||||
});
|
||||
let activation_state = XdgActivationState::new::<State>(&display_handle);
|
||||
event_loop
|
||||
.insert_source(
|
||||
Timer::from_duration(XDG_ACTIVATION_TOKEN_TIMEOUT),
|
||||
|_, _, state| {
|
||||
state.niri.activation_state.retain_tokens(|_, token_data| {
|
||||
token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
|
||||
});
|
||||
TimeoutAction::ToDuration(XDG_ACTIVATION_TOKEN_TIMEOUT)
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mutter_x11_interop_state =
|
||||
MutterX11InteropManagerState::new::<State, _>(&display_handle, move |_| true);
|
||||
|
||||
@@ -1775,7 +1821,7 @@ impl Niri {
|
||||
.unwrap();
|
||||
|
||||
drop(config_);
|
||||
Self {
|
||||
let mut niri = Self {
|
||||
config,
|
||||
config_file_output_config,
|
||||
|
||||
@@ -1797,6 +1843,7 @@ impl Niri {
|
||||
blocker_cleared_tx,
|
||||
blocker_cleared_rx,
|
||||
monitors_active: true,
|
||||
is_lid_closed: false,
|
||||
|
||||
devices: HashSet::new(),
|
||||
tablets: HashMap::new(),
|
||||
@@ -1850,9 +1897,9 @@ impl Niri {
|
||||
cursor_texture_cache: Default::default(),
|
||||
cursor_shape_manager_state,
|
||||
dnd_icon: None,
|
||||
pointer_focus: PointerFocus::default(),
|
||||
pointer_contents: PointContents::default(),
|
||||
pointer_hidden: false,
|
||||
pointer_grab_ongoing: false,
|
||||
pointer_inactivity_timer: None,
|
||||
tablet_cursor_location: None,
|
||||
gesture_swipe_3f_cumulative: None,
|
||||
vertical_wheel_tracker: ScrollTracker::new(120),
|
||||
@@ -1887,11 +1934,19 @@ impl Niri {
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
mapped_cast_output: HashMap::new(),
|
||||
}
|
||||
};
|
||||
|
||||
niri.reset_pointer_inactivity_timer();
|
||||
|
||||
niri
|
||||
}
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub fn inhibit_power_key(&mut self) -> anyhow::Result<()> {
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
|
||||
use smithay::reexports::rustix::io::{fcntl_setfd, FdFlags};
|
||||
|
||||
let conn = zbus::blocking::ConnectionBuilder::system()?.build()?;
|
||||
|
||||
let message = conn.call_method(
|
||||
@@ -1902,7 +1957,14 @@ impl Niri {
|
||||
&("handle-power-key", "niri", "Power key handling", "block"),
|
||||
)?;
|
||||
|
||||
let fd = message.body()?;
|
||||
let fd: zbus::zvariant::OwnedFd = message.body()?;
|
||||
|
||||
// Don't leak the fd to child processes.
|
||||
let borrowed = unsafe { BorrowedFd::borrow_raw(fd.as_raw_fd()) };
|
||||
if let Err(err) = fcntl_setfd(borrowed, FdFlags::CLOEXEC) {
|
||||
warn!("error setting CLOEXEC on inhibit fd: {err:?}");
|
||||
};
|
||||
|
||||
self.inhibit_power_key_fd = Some(fd);
|
||||
|
||||
Ok(())
|
||||
@@ -2284,13 +2346,14 @@ impl Niri {
|
||||
self.window_under(pos)
|
||||
}
|
||||
|
||||
/// Returns the surface under cursor and its position in the global space.
|
||||
/// Returns contents under the given point.
|
||||
///
|
||||
/// Pointer needs location in global space, and focused window location compatible with that
|
||||
/// global space. We don't have a global space for all windows, but this function converts the
|
||||
/// window location temporarily to the current global space.
|
||||
pub fn surface_under_and_global_space(&mut self, pos: Point<f64, Logical>) -> PointerFocus {
|
||||
let mut rv = PointerFocus::default();
|
||||
/// We don't have a proper global space for all windows, so this function converts window
|
||||
/// locations to global space according to where they are rendered.
|
||||
///
|
||||
/// This function does not take pointer or touch grabs into account.
|
||||
pub fn contents_under(&mut self, pos: Point<f64, Logical>) -> PointContents {
|
||||
let mut rv = PointContents::default();
|
||||
|
||||
let Some((output, pos_within_output)) = self.output_under(pos) else {
|
||||
return rv;
|
||||
@@ -2589,22 +2652,20 @@ impl Niri {
|
||||
|
||||
let output_scale = Scale::from(output.current_scale().fractional_scale());
|
||||
|
||||
let (mut pointer_elements, pointer_pos) = match render_cursor {
|
||||
RenderCursor::Hidden => (vec![], pointer_pos.to_physical_precise_round(output_scale)),
|
||||
let mut pointer_elements = match render_cursor {
|
||||
RenderCursor::Hidden => vec![],
|
||||
RenderCursor::Surface { surface, hotspot } => {
|
||||
let pointer_pos =
|
||||
(pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale);
|
||||
|
||||
let pointer_elements = render_elements_from_surface_tree(
|
||||
render_elements_from_surface_tree(
|
||||
renderer,
|
||||
&surface,
|
||||
pointer_pos,
|
||||
output_scale,
|
||||
1.,
|
||||
Kind::Cursor,
|
||||
);
|
||||
|
||||
(pointer_elements, pointer_pos)
|
||||
)
|
||||
}
|
||||
RenderCursor::Named {
|
||||
icon,
|
||||
@@ -2620,7 +2681,7 @@ impl Niri {
|
||||
let mut pointer_elements = vec![];
|
||||
let pointer_element = match MemoryRenderBufferRenderElement::from_buffer(
|
||||
renderer,
|
||||
pointer_pos.to_f64(),
|
||||
pointer_pos,
|
||||
&texture,
|
||||
None,
|
||||
None,
|
||||
@@ -2637,14 +2698,16 @@ impl Niri {
|
||||
pointer_elements.push(OutputRenderElements::NamedPointer(element));
|
||||
}
|
||||
|
||||
(pointer_elements, pointer_pos)
|
||||
pointer_elements
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(dnd_icon) = &self.dnd_icon {
|
||||
if let Some(dnd_icon) = self.dnd_icon.as_ref() {
|
||||
let pointer_pos =
|
||||
(pointer_pos + dnd_icon.offset.to_f64()).to_physical_precise_round(output_scale);
|
||||
pointer_elements.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
dnd_icon,
|
||||
&dnd_icon.surface,
|
||||
pointer_pos,
|
||||
output_scale,
|
||||
1.,
|
||||
@@ -2667,7 +2730,7 @@ impl Niri {
|
||||
.tablet_cursor_location
|
||||
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
|
||||
|
||||
match self.cursor_manager.cursor_image().clone() {
|
||||
match self.cursor_manager.cursor_image() {
|
||||
CursorImageStatus::Surface(ref surface) => {
|
||||
let hotspot = with_states(surface, |states| {
|
||||
states
|
||||
@@ -2685,6 +2748,7 @@ impl Niri {
|
||||
let dnd = self
|
||||
.dnd_icon
|
||||
.as_ref()
|
||||
.map(|icon| &icon.surface)
|
||||
.map(|surface| (surface, bbox_from_surface_tree(surface, surface_pos)));
|
||||
|
||||
// FIXME we basically need to pick the largest scale factor across the overlapping
|
||||
@@ -2746,12 +2810,12 @@ impl Niri {
|
||||
}
|
||||
cursor_image => {
|
||||
// There's no cursor surface, but there might be a DnD icon.
|
||||
let Some(surface) = &self.dnd_icon else {
|
||||
let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let icon = if let CursorImageStatus::Named(icon) = cursor_image {
|
||||
icon
|
||||
*icon
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -2800,6 +2864,23 @@ impl Niri {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_layout(&mut self) {
|
||||
let layout_is_active = match &self.keyboard_focus {
|
||||
KeyboardFocus::Layout { .. } => true,
|
||||
KeyboardFocus::LayerShell { .. } => false,
|
||||
|
||||
// Draw layout as active in these cases to reduce unnecessary window animations.
|
||||
// There's no confusion because these are both fullscreen modes.
|
||||
//
|
||||
// FIXME: when going into the screenshot UI from a layer-shell focus, and then back to
|
||||
// layer-shell, the layout will briefly draw as active, despite never having focus.
|
||||
KeyboardFocus::LockScreen { .. } => true,
|
||||
KeyboardFocus::ScreenshotUi => true,
|
||||
};
|
||||
|
||||
self.layout.refresh(layout_is_active);
|
||||
}
|
||||
|
||||
pub fn refresh_idle_inhibit(&mut self) {
|
||||
let _span = tracy_client::span!("Niri::refresh_idle_inhibit");
|
||||
|
||||
@@ -3019,7 +3100,11 @@ impl Niri {
|
||||
|
||||
// Get monitor elements.
|
||||
let mon = self.layout.monitor_for_output(output).unwrap();
|
||||
let monitor_elements = mon.render_elements(renderer, target);
|
||||
let monitor_elements: Vec<_> = mon.render_elements(renderer, target).collect();
|
||||
let float_elements: Vec<_> = self
|
||||
.layout
|
||||
.render_floating_for_output(renderer, output, target)
|
||||
.collect();
|
||||
|
||||
// Get layer-shell elements.
|
||||
let layer_map = layer_map_for_output(output);
|
||||
@@ -3050,10 +3135,12 @@ impl Niri {
|
||||
|
||||
// Then the regular monitor elements and the top layer in varying order.
|
||||
if mon.render_above_top_layer() {
|
||||
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
|
||||
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
|
||||
extend_from_layer(&mut elements, Layer::Top);
|
||||
} else {
|
||||
extend_from_layer(&mut elements, Layer::Top);
|
||||
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
|
||||
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
|
||||
}
|
||||
|
||||
@@ -3095,11 +3182,7 @@ impl Niri {
|
||||
}
|
||||
}
|
||||
|
||||
state.unfinished_animations_remain = self
|
||||
.layout
|
||||
.monitor_for_output(output)
|
||||
.unwrap()
|
||||
.are_animations_ongoing();
|
||||
state.unfinished_animations_remain = self.layout.are_animations_ongoing(Some(output));
|
||||
|
||||
self.config_error_notification
|
||||
.advance_animations(target_presentation_time);
|
||||
@@ -3268,7 +3351,7 @@ impl Niri {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(surface) = &self.dnd_icon {
|
||||
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
|
||||
with_surface_tree_downward(
|
||||
surface,
|
||||
(),
|
||||
@@ -3406,7 +3489,7 @@ impl Niri {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(surface) = &self.dnd_icon {
|
||||
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
|
||||
send_dmabuf_feedback_surface_tree(
|
||||
surface,
|
||||
output,
|
||||
@@ -3508,7 +3591,7 @@ impl Niri {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(surface) = &self.dnd_icon {
|
||||
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
|
||||
send_frames_surface_tree(
|
||||
surface,
|
||||
output,
|
||||
@@ -3576,7 +3659,7 @@ impl Niri {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(surface) = &self.dnd_icon {
|
||||
if let Some(surface) = &self.dnd_icon.as_ref().map(|icon| &icon.surface) {
|
||||
send_frames_surface_tree(
|
||||
surface,
|
||||
output,
|
||||
@@ -3615,7 +3698,7 @@ impl Niri {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(surface) = &self.dnd_icon {
|
||||
if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) {
|
||||
take_presentation_feedback_surface_tree(
|
||||
surface,
|
||||
&mut feedback,
|
||||
@@ -4427,8 +4510,16 @@ impl Niri {
|
||||
self.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
|
||||
self.lock_state = LockState::Locking(confirmation);
|
||||
self.queue_redraw_all();
|
||||
if self.output_state.is_empty() {
|
||||
// There are no outputs, lock the session right away.
|
||||
let lock = confirmation.ext_session_lock().clone();
|
||||
confirmation.lock();
|
||||
self.lock_state = LockState::Locked(lock);
|
||||
} else {
|
||||
// There are outputs, which we need to redraw before locking.
|
||||
self.lock_state = LockState::Locking(confirmation);
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unlock(&mut self) {
|
||||
@@ -4455,17 +4546,20 @@ impl Niri {
|
||||
output_state.lock_surface = Some(surface);
|
||||
}
|
||||
|
||||
pub fn maybe_activate_pointer_constraint(
|
||||
&self,
|
||||
new_pos: Point<f64, Logical>,
|
||||
new_under: &PointerFocus,
|
||||
) {
|
||||
let Some((surface, surface_loc)) = &new_under.surface else {
|
||||
/// Activates the pointer constraint if necessary according to the current pointer contents.
|
||||
///
|
||||
/// Make sure the pointer location and contents are up to date before calling this.
|
||||
pub fn maybe_activate_pointer_constraint(&self) {
|
||||
let pointer = self.seat.get_pointer().unwrap();
|
||||
let pointer_pos = pointer.current_location();
|
||||
|
||||
let Some((surface, surface_loc)) = &self.pointer_contents.surface else {
|
||||
return;
|
||||
};
|
||||
if self.pointer_grab_ongoing {
|
||||
if Some(surface) != pointer.current_focus().as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pointer = &self.seat.get_pointer().unwrap();
|
||||
with_pointer_constraint(surface, pointer, |constraint| {
|
||||
let Some(constraint) = constraint else { return };
|
||||
@@ -4476,8 +4570,8 @@ impl Niri {
|
||||
|
||||
// Constraint does not apply if not within region.
|
||||
if let Some(region) = constraint.region() {
|
||||
let new_pos_within_surface = new_pos - *surface_loc;
|
||||
if !region.contains(new_pos_within_surface.to_i32_round()) {
|
||||
let pos_within_surface = pointer_pos - *surface_loc;
|
||||
if !region.contains(pos_within_surface.to_i32_round()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4549,7 +4643,7 @@ impl Niri {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) {
|
||||
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointContents) {
|
||||
let Some(ffm) = self.config.borrow().input.focus_follows_mouse else {
|
||||
return;
|
||||
};
|
||||
@@ -4560,7 +4654,7 @@ impl Niri {
|
||||
}
|
||||
|
||||
// Recompute the current pointer focus because we don't update it during animations.
|
||||
let current_focus = self.surface_under_and_global_space(pointer.current_location());
|
||||
let current_focus = self.contents_under(pointer.current_location());
|
||||
|
||||
if let Some(output) = &new_focus.output {
|
||||
if current_focus.output.as_ref() != Some(output) {
|
||||
@@ -4698,6 +4792,32 @@ impl Niri {
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_pointer_inactivity_timer(&mut self) {
|
||||
let _span = tracy_client::span!("Niri::reset_pointer_inactivity_timer");
|
||||
|
||||
if let Some(token) = self.pointer_inactivity_timer.take() {
|
||||
self.event_loop.remove(token);
|
||||
}
|
||||
|
||||
let Some(timeout_ms) = self.config.borrow().cursor.hide_after_inactive_ms else {
|
||||
return;
|
||||
};
|
||||
|
||||
let duration = Duration::from_millis(timeout_ms as u64);
|
||||
let timer = Timer::from_duration(duration);
|
||||
let token = self
|
||||
.event_loop
|
||||
.insert_source(timer, move |_, _, state| {
|
||||
state.niri.pointer_inactivity_timer = None;
|
||||
state.niri.pointer_hidden = true;
|
||||
state.niri.queue_redraw_all();
|
||||
|
||||
TimeoutAction::Drop
|
||||
})
|
||||
.unwrap();
|
||||
self.pointer_inactivity_timer = Some(token);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientState {
|
||||
@@ -4715,6 +4835,7 @@ impl ClientData for ClientState {
|
||||
niri_render_elements! {
|
||||
OutputRenderElements<R> => {
|
||||
Monitor = MonitorRenderElement<R>,
|
||||
Tile = TileRenderElement<R>,
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
NamedPointer = MemoryRenderBufferRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
|
||||
@@ -11,10 +11,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
@@ -22,6 +19,7 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::utils::with_toplevel_role;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
@@ -96,37 +94,23 @@ pub fn refresh(state: &mut State) {
|
||||
// the previous window and only then activate the newly focused window.
|
||||
let mut focused = None;
|
||||
state.niri.layout.with_windows(|mapped, output, _| {
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
let toplevel = mapped.toplevel();
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
|
||||
focused = Some((mapped.window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
|
||||
refresh_toplevel(protocol_state, wl_surface, role, output, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ffi::CString;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -76,38 +76,31 @@ unsafe fn compile_program(
|
||||
let debug_program =
|
||||
unsafe { link_program(gl, include_str!("shaders/texture.vert"), &debug_shader)? };
|
||||
|
||||
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
|
||||
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
|
||||
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
|
||||
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
|
||||
let size = CStr::from_bytes_with_nul(b"niri_size\0").expect("NULL terminated");
|
||||
let scale = CStr::from_bytes_with_nul(b"niri_scale\0").expect("NULL terminated");
|
||||
let alpha = CStr::from_bytes_with_nul(b"niri_alpha\0").expect("NULL terminated");
|
||||
let tint = CStr::from_bytes_with_nul(b"niri_tint\0").expect("NULL terminated");
|
||||
let vert = c"vert";
|
||||
let vert_position = c"vert_position";
|
||||
let matrix = c"matrix";
|
||||
let tex_matrix = c"tex_matrix";
|
||||
let size = c"niri_size";
|
||||
let scale = c"niri_scale";
|
||||
let alpha = c"niri_alpha";
|
||||
let tint = c"niri_tint";
|
||||
|
||||
Ok(ShaderProgram(Rc::new(ShaderProgramInner {
|
||||
normal: ShaderProgramInternal {
|
||||
program,
|
||||
uniform_matrix: gl
|
||||
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tex_matrix: gl
|
||||
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_size: gl
|
||||
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_scale: gl
|
||||
.GetUniformLocation(program, scale.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_alpha: gl
|
||||
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert_position: gl
|
||||
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr()),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(program, tex_matrix.as_ptr()),
|
||||
uniform_size: gl.GetUniformLocation(program, size.as_ptr()),
|
||||
uniform_scale: gl.GetUniformLocation(program, scale.as_ptr()),
|
||||
uniform_alpha: gl.GetUniformLocation(program, alpha.as_ptr()),
|
||||
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
|
||||
attrib_vert_position: gl.GetAttribLocation(program, vert_position.as_ptr()),
|
||||
additional_uniforms: additional_uniforms
|
||||
.iter()
|
||||
.map(|uniform| {
|
||||
let name =
|
||||
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
|
||||
let location =
|
||||
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||
let location = gl.GetUniformLocation(program, name.as_ptr());
|
||||
(
|
||||
uniform.name.clone().into_owned(),
|
||||
UniformDesc {
|
||||
@@ -121,41 +114,26 @@ unsafe fn compile_program(
|
||||
.iter()
|
||||
.map(|name_| {
|
||||
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
|
||||
let location =
|
||||
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||
let location = gl.GetUniformLocation(program, name.as_ptr());
|
||||
(name_.to_string(), location)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
debug: ShaderProgramInternal {
|
||||
program: debug_program,
|
||||
uniform_matrix: gl
|
||||
.GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(
|
||||
debug_program,
|
||||
tex_matrix.as_ptr() as *const ffi::types::GLchar,
|
||||
),
|
||||
uniform_size: gl
|
||||
.GetUniformLocation(debug_program, size.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_scale: gl
|
||||
.GetUniformLocation(debug_program, scale.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_alpha: gl
|
||||
.GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert: gl
|
||||
.GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert_position: gl.GetAttribLocation(
|
||||
debug_program,
|
||||
vert_position.as_ptr() as *const ffi::types::GLchar,
|
||||
),
|
||||
uniform_matrix: gl.GetUniformLocation(debug_program, matrix.as_ptr()),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(debug_program, tex_matrix.as_ptr()),
|
||||
uniform_size: gl.GetUniformLocation(debug_program, size.as_ptr()),
|
||||
uniform_scale: gl.GetUniformLocation(debug_program, scale.as_ptr()),
|
||||
uniform_alpha: gl.GetUniformLocation(debug_program, alpha.as_ptr()),
|
||||
attrib_vert: gl.GetAttribLocation(debug_program, vert.as_ptr()),
|
||||
attrib_vert_position: gl.GetAttribLocation(debug_program, vert_position.as_ptr()),
|
||||
additional_uniforms: additional_uniforms
|
||||
.iter()
|
||||
.map(|uniform| {
|
||||
let name =
|
||||
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
|
||||
let location = gl.GetUniformLocation(
|
||||
debug_program,
|
||||
name.as_ptr() as *const ffi::types::GLchar,
|
||||
);
|
||||
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
|
||||
(
|
||||
uniform.name.clone().into_owned(),
|
||||
UniformDesc {
|
||||
@@ -169,16 +147,12 @@ unsafe fn compile_program(
|
||||
.iter()
|
||||
.map(|name_| {
|
||||
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
|
||||
let location = gl.GetUniformLocation(
|
||||
debug_program,
|
||||
name.as_ptr() as *const ffi::types::GLchar,
|
||||
);
|
||||
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
|
||||
(name_.to_string(), location)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
uniform_tint: gl
|
||||
.GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tint: gl.GetUniformLocation(debug_program, tint.as_ptr()),
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -211,7 +185,7 @@ impl ShaderRenderElement {
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'_>>,
|
||||
additional_uniforms: Vec<Uniform<'static>>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
@@ -223,7 +197,7 @@ impl ShaderRenderElement {
|
||||
opaque_regions: opaque_regions.unwrap_or_default(),
|
||||
scale,
|
||||
alpha,
|
||||
additional_uniforms: uniforms.into_iter().map(|u| u.into_owned()).collect(),
|
||||
additional_uniforms,
|
||||
textures,
|
||||
kind,
|
||||
}
|
||||
@@ -253,13 +227,13 @@ impl ShaderRenderElement {
|
||||
size: Size<f64, Logical>,
|
||||
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
|
||||
scale: f32,
|
||||
uniforms: Vec<Uniform<'_>>,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
) {
|
||||
self.area.size = size;
|
||||
self.opaque_regions = opaque_regions.unwrap_or_default();
|
||||
self.scale = scale;
|
||||
self.additional_uniforms = uniforms.into_iter().map(|u| u.into_owned()).collect();
|
||||
self.additional_uniforms = uniforms;
|
||||
self.textures = textures;
|
||||
|
||||
self.commit_counter.increment();
|
||||
|
||||
+24
-1
@@ -17,8 +17,11 @@ use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Coordinate, Logical, Point, Rectangle, Size, Transform};
|
||||
use smithay::wayland::compositor::{send_surface_state, SurfaceData};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states, SurfaceData};
|
||||
use smithay::wayland::fractional_scale::with_fractional_scale;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
|
||||
pub mod id;
|
||||
pub mod scale;
|
||||
@@ -221,6 +224,26 @@ pub fn output_matches_name(output: &Output, target: &str) -> bool {
|
||||
name.matches(target)
|
||||
}
|
||||
|
||||
pub fn is_laptop_panel(connector: &str) -> bool {
|
||||
matches!(connector.get(..4), Some("eDP-" | "LVDS" | "DSI-"))
|
||||
}
|
||||
|
||||
pub fn with_toplevel_role<T>(
|
||||
toplevel: &ToplevelSurface,
|
||||
f: impl FnOnce(&mut XdgToplevelSurfaceRoleAttributes) -> T,
|
||||
) -> T {
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
let mut role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
f(&mut role)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
|
||||
let mut notification = notify_rust::Notification::new();
|
||||
|
||||
+9
-15
@@ -15,7 +15,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource as _;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
|
||||
use smithay::wayland::compositor::{remove_pre_commit_hook, with_states, HookId};
|
||||
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface, XdgToplevelSurfaceData};
|
||||
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
|
||||
|
||||
use super::{ResolvedWindowRules, WindowRef};
|
||||
use crate::handlers::KdeDecorationsModeState;
|
||||
@@ -33,7 +33,7 @@ use crate::render_helpers::surface::render_snapshot_from_surface_tree;
|
||||
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{send_scale_transform, ResizeEdge};
|
||||
use crate::utils::{send_scale_transform, with_toplevel_role, ResizeEdge};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mapped {
|
||||
@@ -571,7 +571,7 @@ impl LayoutElement for Mapped {
|
||||
|
||||
fn has_ssd(&self) -> bool {
|
||||
let toplevel = self.toplevel();
|
||||
let mode = toplevel.current_state().decoration_mode;
|
||||
let mode = with_toplevel_role(self.toplevel(), |role| role.current.decoration_mode);
|
||||
|
||||
match mode {
|
||||
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) => true,
|
||||
@@ -631,14 +631,7 @@ impl LayoutElement for Mapped {
|
||||
let _span =
|
||||
trace_span!("configure_intent", surface = ?self.toplevel().wl_surface().id()).entered();
|
||||
|
||||
with_states(self.toplevel().wl_surface(), |states| {
|
||||
let attributes = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
with_toplevel_role(self.toplevel(), |attributes| {
|
||||
if let Some(server_pending) = &attributes.server_pending {
|
||||
let current_server = attributes.current_server_state();
|
||||
if server_pending != current_server {
|
||||
@@ -719,10 +712,11 @@ impl LayoutElement for Mapped {
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
self.toplevel()
|
||||
.current_state()
|
||||
.states
|
||||
.contains(xdg_toplevel::State::Fullscreen)
|
||||
with_toplevel_role(self.toplevel(), |role| {
|
||||
role.current
|
||||
.states
|
||||
.contains(xdg_toplevel::State::Fullscreen)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_pending_fullscreen(&self) -> bool {
|
||||
|
||||
+4
-14
@@ -1,11 +1,9 @@
|
||||
use niri_config::{BlockOutFrom, BorderRule, CornerRadius, Match, WindowRule};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::{ToplevelSurface, XdgToplevelSurfaceRoleAttributes};
|
||||
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
use crate::utils::with_toplevel_role;
|
||||
|
||||
pub mod mapped;
|
||||
pub use mapped::Mapped;
|
||||
@@ -144,15 +142,7 @@ impl ResolvedWindowRules {
|
||||
|
||||
let mut resolved = ResolvedWindowRules::empty();
|
||||
|
||||
let toplevel = window.toplevel();
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
let mut role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
with_toplevel_role(window.toplevel(), |role| {
|
||||
// Ensure server_pending like in Smithay's with_pending_state().
|
||||
if role.server_pending.is_none() {
|
||||
role.server_pending = Some(role.current_server_state().clone());
|
||||
@@ -169,7 +159,7 @@ impl ResolvedWindowRules {
|
||||
}
|
||||
}
|
||||
|
||||
window_matches(window, &role, m)
|
||||
window_matches(window, role, m)
|
||||
};
|
||||
|
||||
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
### VSCode
|
||||
|
||||
There seems to be a bug in VSCode's Wayland backend until 1.86.0 which causes the window to not show up when using server-side decorations. So, to run VSCode:
|
||||
|
||||
1. Make sure VSCode is 1.86.0 or above, or that `prefer-no-csd` is **not set** in the niri config
|
||||
2. Run `code --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations`
|
||||
|
||||
Also, if you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode. That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance. Apparently, VSCode currently unconditionally queries the X server for a keymap.
|
||||
|
||||
### Chromium
|
||||
|
||||
When creating new windows within Chromium (e.g. with <kbd>Ctrl</kbd><kbd>N</kbd>), there's a Chromium bug with sizing:
|
||||
|
||||
- With CSD (`prefer-no-csd` unset), the window will be a bit smaller than needed
|
||||
- With SSD (`prefer-no-csd` set), the window buffer will be offset to the top-left
|
||||
|
||||
Both of these can be fixed by resizing the new Chromium window.
|
||||
If you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode.
|
||||
That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance.
|
||||
Apparently, VSCode currently unconditionally queries the X server for a keymap.
|
||||
|
||||
### WezTerm
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ debug {
|
||||
emulate-zero-presentation-time
|
||||
disable-resize-throttling
|
||||
disable-transactions
|
||||
keep-laptop-panel-on-when-lid-is-closed
|
||||
disable-monitor-names
|
||||
}
|
||||
|
||||
binds {
|
||||
@@ -165,6 +167,33 @@ debug {
|
||||
}
|
||||
```
|
||||
|
||||
### `keep-laptop-panel-on-when-lid-is-closed`
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
By default, niri will disable the internal laptop monitor when the laptop lid is closed.
|
||||
This flag turns off this behavior and will leave the internal laptop monitor on.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
keep-laptop-panel-on-when-lid-is-closed
|
||||
}
|
||||
```
|
||||
|
||||
### `disable-monitor-names`
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
Disables the make/model/serial monitor names, as if niri fails to read them from the EDID.
|
||||
|
||||
Use this flag to work around a crash present in 0.1.9 and 0.1.10 when connecting two monitors with matching make/model/serial.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
disable-monitor-names
|
||||
}
|
||||
```
|
||||
|
||||
### Key Bindings
|
||||
|
||||
These are not debug options, but rather key bindings.
|
||||
|
||||
@@ -32,7 +32,9 @@ input {
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-factor 1.0
|
||||
// scroll-method "two-finger"
|
||||
// scroll-button 273
|
||||
// tap-button-map "left-middle-right"
|
||||
// click-method "clickfinger"
|
||||
// left-handed
|
||||
@@ -45,7 +47,9 @@ input {
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-factor 1.0
|
||||
// scroll-method "no-scroll"
|
||||
// scroll-button 273
|
||||
// left-handed
|
||||
// middle-emulation
|
||||
}
|
||||
@@ -56,6 +60,18 @@ input {
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
trackball {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// left-handed
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
@@ -134,13 +150,14 @@ A few settings are common between input devices:
|
||||
|
||||
- `off`: if set, no events will be sent from this device.
|
||||
|
||||
A few settings are common between `touchpad`, `mouse` and `trackpoint`:
|
||||
A few settings are common between `touchpad`, `mouse`, `trackpoint`, and `trackball`:
|
||||
|
||||
- `natural-scroll`: if set, inverts the scrolling direction.
|
||||
- `accel-speed`: pointer acceleration speed, valid values are from `-1.0` to `1.0` where the default is `0.0`.
|
||||
- `accel-profile`: can be `adaptive` (the default) or `flat` (disables pointer acceleration).
|
||||
- `scroll-method`: when to generate scroll events instead of pointer motion events, can be `no-scroll`, `two-finger`, `edge`, or `on-button-down`.
|
||||
The default and supported methods vary depending on the device type.
|
||||
- `scroll-button`: <sup>Since: 0.1.10</sup> the button code used for the `on-button-down` scroll method. You can find it in `libinput debug-events`.
|
||||
- `middle-emulation`: emulate a middle mouse click by pressing left and right mouse buttons at once.
|
||||
|
||||
Settings specific to `touchpad`s:
|
||||
@@ -152,6 +169,10 @@ Settings specific to `touchpad`s:
|
||||
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
|
||||
- `disabled-on-external-mouse`: do not send events while external pointer device is plugged in.
|
||||
|
||||
Settings specific to `touchpad` and `mouse`:
|
||||
|
||||
- `scroll-factor`: <sup>Since: 0.1.10</sup> scales the scrolling speed by this value.
|
||||
|
||||
Settings specific to `touchpad`, `mouse` and `tablet`:
|
||||
|
||||
- `left-handed`: if set, changes the device to left-handed mode.
|
||||
|
||||
@@ -42,6 +42,12 @@ layout {
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
|
||||
}
|
||||
|
||||
insert-hint {
|
||||
// off
|
||||
color "#ffc87f80"
|
||||
// gradient from="#ffbb6680" to="#ffc88080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
struts {
|
||||
// left 64
|
||||
// right 64
|
||||
@@ -303,6 +309,26 @@ layout {
|
||||
}
|
||||
```
|
||||
|
||||
### `insert-hint`
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
Settings for the window insert position hint during an interactive window move.
|
||||
|
||||
`off` disables the insert hint altogether.
|
||||
|
||||
`color` and `gradient` let you change the color of the hint and have the same syntax as colors and gradients in border and focus ring.
|
||||
|
||||
```kdl
|
||||
layout {
|
||||
insert-hint {
|
||||
// off
|
||||
color "#ffc87f80"
|
||||
gradient from="#ffbb6680" to="#ffc88080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `struts`
|
||||
|
||||
Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||
|
||||
@@ -20,6 +20,9 @@ environment {
|
||||
cursor {
|
||||
xcursor-theme "breeze_cursors"
|
||||
xcursor-size 48
|
||||
|
||||
hide-when-typing
|
||||
hide-after-inactive-ms 1000
|
||||
}
|
||||
|
||||
hotkey-overlay {
|
||||
@@ -106,6 +109,31 @@ cursor {
|
||||
}
|
||||
```
|
||||
|
||||
#### `hide-when-typing`
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
If set, hides the cursor when pressing a key on the keyboard.
|
||||
|
||||
```kdl
|
||||
cursor {
|
||||
hide-when-typing
|
||||
}
|
||||
```
|
||||
|
||||
#### `hide-after-inactive-ms`
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
If set, the cursor will automatically hide once this number of milliseconds passes since the last cursor movement.
|
||||
|
||||
```kdl
|
||||
cursor {
|
||||
// Hide the cursor after one second of inactivity.
|
||||
hide-after-inactive-ms 1000
|
||||
}
|
||||
```
|
||||
|
||||
### `hotkey-overlay`
|
||||
|
||||
Settings for the "Important Hotkeys" overlay.
|
||||
|
||||
@@ -5,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)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
### Overview
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
Switch event bindings are declared in the `switch-events {}` section of the config.
|
||||
|
||||
Here are all the events that you can bind at a glance:
|
||||
|
||||
```kdl
|
||||
switch-events {
|
||||
lid-close { spawn "notify-send" "The laptop lid is closed!"; }
|
||||
lid-open { spawn "notify-send" "The laptop lid is open!"; }
|
||||
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
|
||||
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
|
||||
}
|
||||
```
|
||||
|
||||
The syntax is similar to key bindings.
|
||||
Currently, only the `spawn` action are supported.
|
||||
|
||||
> [!NOTE]
|
||||
> In contrast to key bindings, switch event bindings are *always* executed, even when the session is locked.
|
||||
|
||||
### `lid-close`, `lid-open`
|
||||
|
||||
These events correspond to closing and opening of the laptop lid.
|
||||
|
||||
Note that niri will already automatically turn the internal laptop monitor on and off in accordance with the laptop lid.
|
||||
|
||||
```kdl
|
||||
switch-events {
|
||||
lid-close { spawn "notify-send" "The laptop lid is closed!"; }
|
||||
lid-open { spawn "notify-send" "The laptop lid is open!"; }
|
||||
}
|
||||
```
|
||||
|
||||
### `tablet-mode-on`, `tablet-mode-off`
|
||||
|
||||
These events trigger when a convertible laptop goes into or out of tablet mode.
|
||||
In tablet mode, the keyboard and mouse are usually inaccessible, so you can use these events to activate the on-screen keyboard.
|
||||
|
||||
```kdl
|
||||
switch-events {
|
||||
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
|
||||
tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; }
|
||||
}
|
||||
```
|
||||
@@ -72,3 +72,5 @@ pub fn some_function() {
|
||||
// Code of the function.
|
||||
}
|
||||
```
|
||||
|
||||
You can also enable Rust memory allocation profiling with `--features=profile-with-tracy-allocations`.
|
||||
|
||||
@@ -46,3 +46,10 @@ hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
```
|
||||
|
||||
### How to run X11 apps like Steam or Discord?
|
||||
|
||||
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
|
||||
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
|
||||
|
||||
Keep in mind that you can run many Electron apps such as VSCode natively on Wayland by passing the right flags, e.g. `code --ozone-platform-hint=auto`
|
||||
|
||||
@@ -4,6 +4,14 @@ There are several gestures in niri.
|
||||
|
||||
### Mouse
|
||||
|
||||
#### Interactive Move
|
||||
|
||||
<sup>Since: 0.1.10</sup>
|
||||
|
||||
You can move windows by holding <kbd>Mod</kbd> and the left mouse button.
|
||||
|
||||
You can customize the look of the window insertion preview in the `insert-hint` [layout config](./Configuration:-Layout.md) section.
|
||||
|
||||
#### Interactive Resize
|
||||
|
||||
<sup>Since: 0.1.6</sup>
|
||||
|
||||
+13
-15
@@ -8,8 +8,9 @@ After installing, start niri from your display manager like GDM.
|
||||
Press <kbd>Super</kbd><kbd>T</kbd> to run a terminal ([Alacritty]) and <kbd>Super</kbd><kbd>D</kbd> to run an application launcher ([fuzzel]).
|
||||
To exit niri, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
|
||||
|
||||
If you're not using a display manager, you should run `niri-session` (systemd) or `niri --session` (not systemd) from a TTY.
|
||||
The `--session` flag will make niri import its environment variables globally into systemd and D-Bus, and start its D-Bus services.
|
||||
If you're not using a display manager, you should run `niri-session` (systemd/dinit) or `niri --session` (others) from a TTY.
|
||||
The `--session` flag will make niri import its environment variables globally into the system manager and D-Bus, and start its D-Bus services.
|
||||
The `niri-session` script will additionally start niri as a systemd/dinit service, which starts up a graphical session target required by some services like portals.
|
||||
|
||||
You can also run `niri` inside an existing desktop session.
|
||||
Then it will open as a window, where you can give it a try.
|
||||
@@ -22,21 +23,13 @@ Finally, the [Xwayland](./Xwayland.md) page explains how to run X11 applications
|
||||
|
||||
### NVIDIA
|
||||
|
||||
NVIDIA GPUs tend to have problems running niri (for example, the screen remains black upon starting from a TTY).
|
||||
NVIDIA GPUs can have problems running niri (for example, the screen remains black upon starting from a TTY).
|
||||
Sometimes, the problems can be fixed.
|
||||
You can try the following:
|
||||
|
||||
1. Update NVIDIA drivers. You need a GPU and drivers recent enough to support GBM.
|
||||
2. Make sure kernel modesetting is enabled. This usually involves adding `nvidia-drm.modeset=1` to the kernel command line. Find and follow a guide for your distribution. Guides from other Wayland compositors can help.
|
||||
|
||||
If niri runs but the screen flickers, try adding this into your niri config:
|
||||
|
||||
```
|
||||
debug {
|
||||
wait-for-frame-completion-before-queueing
|
||||
}
|
||||
```
|
||||
|
||||
### Asahi, ARM, and other kmsro devices
|
||||
|
||||
On some of these systems, niri fails to correctly detect the primary render device.
|
||||
@@ -69,6 +62,7 @@ If you still get a black screen, try using each of the `card` devices.
|
||||
### Nix/NixOS
|
||||
|
||||
There's a common problem of mesa drivers going out of sync with niri, so make sure your system mesa version matches the niri mesa version.
|
||||
When this happens, you usually see a black screen when trying to start niri from a TTY.
|
||||
|
||||
Also, on Intel graphics, you may need a workaround described [here](https://nixos.wiki/wiki/Intel_Graphics).
|
||||
|
||||
@@ -178,8 +172,10 @@ To do that, put files into the correct directories according to this table.
|
||||
| `resources/niri-session` | `/usr/bin/` |
|
||||
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
|
||||
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
|
||||
| `resources/niri.service` | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri.service` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
|
||||
Doing this will make niri appear in GDM and other display managers.
|
||||
|
||||
@@ -195,8 +191,10 @@ These may vary depending on your distribution.
|
||||
| `resources/niri-session` | `/usr/local/bin/` |
|
||||
| `resources/niri.desktop` | `/usr/local/share/wayland-sessions/` |
|
||||
| `resources/niri-portals.conf` | `/usr/local/share/xdg-desktop-portal/` |
|
||||
| `resources/niri.service` | `/etc/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` | `/etc/systemd/user/` |
|
||||
| `resources/niri.service` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/etc/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/etc/dinit.d/user/` |
|
||||
|
||||
[Alacritty]: https://github.com/alacritty/alacritty
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
@@ -33,3 +33,10 @@ systemctl --user edit --full plasma-polkit-agent.service
|
||||
```
|
||||
|
||||
Then add `After=graphical-session.target`.
|
||||
|
||||
### Xwayland
|
||||
|
||||
To run X11 apps like Steam or Discord, you can use [xwayland-satellite].
|
||||
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
|
||||
|
||||
[xwayland-satellite]: https://github.com/Supreeeme/xwayland-satellite
|
||||
|
||||
@@ -14,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)
|
||||
|
||||
Reference in New Issue
Block a user