mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae89cb6017 | |||
| b6fc4d0455 | |||
| d8265ad34e | |||
| 3b864dc104 | |||
| 15093221ed | |||
| ac7b3fbf19 | |||
| bb8eb377c7 | |||
| 6169c0312a | |||
| 2ae99224ab | |||
| 4f63e13385 | |||
| 46a8f81160 | |||
| 0d6843ea67 | |||
| 6d083ea497 | |||
| 7a42140d6c | |||
| eeb411bef5 | |||
| defd4c5c4d | |||
| 7227e64149 | |||
| c98537a2b0 | |||
| 9c103f1f1d | |||
| 2aff1ec71a | |||
| 3466fc0a66 | |||
| f917932b3e | |||
| 89b7423ee5 | |||
| a2efaf2816 | |||
| 5816691460 | |||
| 4b5e9e6cb0 | |||
| a8259b4cea | |||
| 9d3d7cb0e9 | |||
| 398bc78ea0 | |||
| caa6189448 | |||
| 86f57c2ec7 | |||
| 3cc67897af | |||
| a99489c6c0 | |||
| 0763c7e196 | |||
| fb5c5204e8 | |||
| d207cd385b | |||
| 99bf2df2b4 | |||
| 09be90f4e6 | |||
| dfc42b9d82 | |||
| e2b9838d89 | |||
| 816a0d479c | |||
| 84323d10a4 | |||
| b956f2775c | |||
| 9ff2f83db0 | |||
| 7a10f71ee5 | |||
| ea7add3563 | |||
| e9c6f08906 | |||
| 17343a6740 | |||
| 140d726cd3 | |||
| c37d3b3442 | |||
| 497f186422 | |||
| 3e31c134a6 | |||
| fe682938db | |||
| 6142922ca4 | |||
| 4b44fba14c | |||
| 57639ca84c | |||
| ec88aae77d | |||
| 6c9705dd4b | |||
| eb590c5346 | |||
| 02baad91ac | |||
| 68589cd5a1 | |||
| f2c690802b | |||
| 9d6037b94c | |||
| 7b4cf094ef | |||
| 446bc155ce | |||
| 3289324ce4 | |||
| 9fb02b9571 | |||
| 0e9496b01e | |||
| 82dabc21f3 | |||
| 39b3d62873 | |||
| af080a03cd | |||
| 5f117c61dc | |||
| cb857e32e4 | |||
| 199be26947 | |||
| d5c0c74d2c | |||
| 9bb292ec82 | |||
| a1ba6bcaa0 | |||
| fd389af6d8 | |||
| db09727b18 | |||
| c9d6478c3c | |||
| 758cca5432 | |||
| 78e3daf5f8 | |||
| a99a0b2492 | |||
| bfd42c74f4 | |||
| 501ea47128 | |||
| d2a1cf53b4 | |||
| 62d47d77d5 | |||
| 85cd64e830 | |||
| 55c14eebf2 | |||
| 3fe67549b4 | |||
| 1835b532d9 | |||
| e6d82d3ee3 | |||
| fae3a27641 | |||
| 31e76cf451 | |||
| b8a9be542f | |||
| 59de6918b3 | |||
| bd3d554389 | |||
| af1fca35bb | |||
| 9571d149b2 | |||
| 99358e36b3 | |||
| 8b878f355f | |||
| 395b6d9a4f | |||
| 25f24f668c | |||
| 929eaf0d69 | |||
| ce3103949f | |||
| ef60dd81d7 | |||
| 7671a5d833 | |||
| 3f09352067 | |||
| 5059cce886 | |||
| b20dd226c0 | |||
| acb69c3b4d | |||
| dbe0a9e293 | |||
| d3a79faeec | |||
| 21630ddb5e | |||
| 9e5e0c85bb | |||
| 5cd8040d1a | |||
| 86351938f2 | |||
| ee4c5e23ab | |||
| ffd6acc0aa | |||
| cee11dc329 | |||
| 59a42249a4 |
@@ -1 +1,12 @@
|
||||
# LFS configuration for images from the wiki
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Exclude LFS-tracked files from the tarball
|
||||
/wiki/img/ export-ignore
|
||||
|
||||
# exclude .gitattributes itself from the tarball
|
||||
.gitattributes export-ignore
|
||||
|
||||
# tip: can be tested using
|
||||
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
|
||||
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
|
||||
|
||||
@@ -3,7 +3,7 @@ updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
groups:
|
||||
smithay:
|
||||
patterns:
|
||||
@@ -17,6 +17,6 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
|
||||
Generated
+254
-237
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -6,7 +6,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.2.0"
|
||||
version = "25.5.0"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -15,10 +15,10 @@ repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.80.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.97"
|
||||
bitflags = "2.9.0"
|
||||
clap = { version = "4.5.34", features = ["derive"] }
|
||||
insta = "1.42.2"
|
||||
anyhow = "1.0.98"
|
||||
bitflags = "2.9.1"
|
||||
clap = { version = "4.5.38", features = ["derive"] }
|
||||
insta = "1.43.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
@@ -56,26 +56,26 @@ async-channel = "2.3.1"
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.22.0", features = ["derive"] }
|
||||
bytemuck = { version = "1.23.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.47"
|
||||
clap_complete = "4.5.50"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.30.1"
|
||||
glam = "0.30.3"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.171"
|
||||
libc = "0.2.172"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.2.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
|
||||
niri-config = { version = "25.5.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.5.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.0.0"
|
||||
pango = { version = "0.20.9", features = ["v1_44"] }
|
||||
pangocairo = "0.20.7"
|
||||
pango = { version = "0.20.10", features = ["v1_44"] }
|
||||
pangocairo = "0.20.10"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
|
||||
@@ -88,10 +88,10 @@ tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.4", optional = true }
|
||||
wayland-backend = "0.3.8"
|
||||
wayland-backend = "0.3.10"
|
||||
wayland-scanner = "0.31.6"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "5.5.0", optional = true }
|
||||
zbus = { version = "5.7.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -118,7 +118,7 @@ insta.workspace = true
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||
rayon = "1.10.0"
|
||||
wayland-client = "0.31.8"
|
||||
wayland-client = "0.31.10"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -30,9 +30,11 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
|
||||
- Built from the ground up for scrollable tiling
|
||||
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
|
||||
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
|
||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
@@ -72,7 +74,7 @@ I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||
- Discord and other Electron apps: work well through xwayland-satellite.
|
||||
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
|
||||
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
|
||||
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
|
||||
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
|
||||
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||
|
||||
## Inspiration
|
||||
@@ -88,8 +90,8 @@ Here are some other projects which implement a similar workflow:
|
||||
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Media
|
||||
@@ -108,7 +110,7 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
||||
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
|
||||
@@ -12,7 +12,7 @@ bitflags.workspace = true
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
|
||||
niri-ipc = { version = "25.5.0", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -15,6 +15,10 @@ pub struct LayerRule {
|
||||
pub shadow: ShadowRule,
|
||||
#[knuffel(child)]
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub place_within_backdrop: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub baba_is_float: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
|
||||
+336
-17
@@ -23,7 +23,8 @@ use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
||||
use smithay::input::keyboard::{Keysym, XkbConfig};
|
||||
use smithay::reexports::input;
|
||||
|
||||
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);
|
||||
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]);
|
||||
pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]);
|
||||
|
||||
pub mod layer_rule;
|
||||
|
||||
@@ -61,6 +62,8 @@ pub struct Config {
|
||||
#[knuffel(child, default)]
|
||||
pub gestures: Gestures,
|
||||
#[knuffel(child, default)]
|
||||
pub overview: Overview,
|
||||
#[knuffel(child, default)]
|
||||
pub environment: Environment,
|
||||
#[knuffel(children(name = "window-rule"))]
|
||||
pub window_rules: Vec<WindowRule>,
|
||||
@@ -117,6 +120,8 @@ pub struct Keyboard {
|
||||
pub repeat_rate: u8,
|
||||
#[knuffel(child, unwrap(argument), default)]
|
||||
pub track_layout: TrackLayout,
|
||||
#[knuffel(child)]
|
||||
pub numlock: bool,
|
||||
}
|
||||
|
||||
impl Default for Keyboard {
|
||||
@@ -126,6 +131,7 @@ impl Default for Keyboard {
|
||||
repeat_delay: 600,
|
||||
repeat_rate: 25,
|
||||
track_layout: Default::default(),
|
||||
numlock: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,8 +448,10 @@ pub struct Output {
|
||||
pub variable_refresh_rate: Option<Vrr>,
|
||||
#[knuffel(child)]
|
||||
pub focus_at_startup: bool,
|
||||
#[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)]
|
||||
pub background_color: Color,
|
||||
#[knuffel(child)]
|
||||
pub background_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub backdrop_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
@@ -471,7 +479,8 @@ impl Default for Output {
|
||||
position: None,
|
||||
mode: None,
|
||||
variable_refresh_rate: None,
|
||||
background_color: DEFAULT_BACKGROUND_COLOR,
|
||||
background_color: None,
|
||||
backdrop_color: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,6 +541,8 @@ pub struct Layout {
|
||||
pub gaps: FloatOrInt<0, 65535>,
|
||||
#[knuffel(child, default)]
|
||||
pub struts: Struts,
|
||||
#[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)]
|
||||
pub background_color: Color,
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
@@ -551,6 +562,7 @@ impl Default for Layout {
|
||||
gaps: FloatOrInt(16.),
|
||||
struts: Default::default(),
|
||||
preset_window_heights: Default::default(),
|
||||
background_color: DEFAULT_BACKGROUND_COLOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,10 +583,14 @@ pub struct FocusRing {
|
||||
pub active_color: Color,
|
||||
#[knuffel(child, default = Self::default().inactive_color)]
|
||||
pub inactive_color: Color,
|
||||
#[knuffel(child, default = Self::default().urgent_color)]
|
||||
pub urgent_color: Color,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
impl Default for FocusRing {
|
||||
@@ -584,8 +600,10 @@ impl Default for FocusRing {
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::from_rgba8_unpremul(127, 200, 255, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
|
||||
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -657,10 +675,14 @@ pub struct Border {
|
||||
pub active_color: Color,
|
||||
#[knuffel(child, default = Self::default().inactive_color)]
|
||||
pub inactive_color: Color,
|
||||
#[knuffel(child, default = Self::default().urgent_color)]
|
||||
pub urgent_color: Color,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
impl Default for Border {
|
||||
@@ -670,8 +692,10 @@ impl Default for Border {
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 200, 127, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
|
||||
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,8 +707,10 @@ impl From<Border> for FocusRing {
|
||||
width: value.width,
|
||||
active_color: value.active_color,
|
||||
inactive_color: value.inactive_color,
|
||||
urgent_color: value.urgent_color,
|
||||
active_gradient: value.active_gradient,
|
||||
inactive_gradient: value.inactive_gradient,
|
||||
urgent_gradient: value.urgent_gradient,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -696,8 +722,10 @@ impl From<FocusRing> for Border {
|
||||
width: value.width,
|
||||
active_color: value.active_color,
|
||||
inactive_color: value.inactive_color,
|
||||
urgent_color: value.urgent_color,
|
||||
active_gradient: value.active_gradient,
|
||||
inactive_gradient: value.inactive_gradient,
|
||||
urgent_gradient: value.urgent_gradient,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,6 +773,49 @@ pub struct ShadowOffset {
|
||||
pub y: FloatOrInt<-65535, 65535>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct WorkspaceShadow {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, default = Self::default().offset)]
|
||||
pub offset: ShadowOffset,
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().softness)]
|
||||
pub softness: FloatOrInt<0, 1024>,
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().spread)]
|
||||
pub spread: FloatOrInt<-1024, 1024>,
|
||||
#[knuffel(child, default = Self::default().color)]
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Default for WorkspaceShadow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
offset: ShadowOffset {
|
||||
x: FloatOrInt(0.),
|
||||
y: FloatOrInt(10.),
|
||||
},
|
||||
softness: FloatOrInt(40.),
|
||||
spread: FloatOrInt(10.),
|
||||
color: Color::from_rgba8_unpremul(0, 0, 0, 0x50),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorkspaceShadow> for Shadow {
|
||||
fn from(value: WorkspaceShadow) -> Self {
|
||||
Self {
|
||||
on: !value.off,
|
||||
offset: value.offset,
|
||||
softness: value.softness,
|
||||
spread: value.spread,
|
||||
draw_behind_window: false,
|
||||
color: value.color,
|
||||
inactive_color: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct TabIndicator {
|
||||
#[knuffel(child)]
|
||||
@@ -770,9 +841,13 @@ pub struct TabIndicator {
|
||||
#[knuffel(child)]
|
||||
pub inactive_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
impl Default for TabIndicator {
|
||||
@@ -791,8 +866,10 @@ impl Default for TabIndicator {
|
||||
corner_radius: FloatOrInt(0.),
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,6 +1061,8 @@ pub struct Animations {
|
||||
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
|
||||
#[knuffel(child, default)]
|
||||
pub screenshot_ui_open: ScreenshotUiOpenAnim,
|
||||
#[knuffel(child, default)]
|
||||
pub overview_open_close: OverviewOpenCloseAnim,
|
||||
}
|
||||
|
||||
impl Default for Animations {
|
||||
@@ -999,6 +1078,7 @@ impl Default for Animations {
|
||||
window_resize: Default::default(),
|
||||
config_notification_open_close: Default::default(),
|
||||
screenshot_ui_open: Default::default(),
|
||||
overview_open_close: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1146,6 +1226,22 @@ impl Default for ScreenshotUiOpenAnim {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct OverviewOpenCloseAnim(pub Animation);
|
||||
|
||||
impl Default for OverviewOpenCloseAnim {
|
||||
fn default() -> Self {
|
||||
Self(Animation {
|
||||
off: false,
|
||||
kind: AnimationKind::Spring(SpringParams {
|
||||
damping_ratio: 1.,
|
||||
stiffness: 800,
|
||||
epsilon: 0.0001,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub off: bool,
|
||||
@@ -1183,6 +1279,10 @@ pub struct SpringParams {
|
||||
pub struct Gestures {
|
||||
#[knuffel(child, default)]
|
||||
pub dnd_edge_view_scroll: DndEdgeViewScroll,
|
||||
#[knuffel(child, default)]
|
||||
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
|
||||
#[knuffel(child, default)]
|
||||
pub hot_corners: HotCorners,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
@@ -1205,6 +1305,52 @@ impl Default for DndEdgeViewScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct DndEdgeWorkspaceSwitch {
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().trigger_height)]
|
||||
pub trigger_height: FloatOrInt<0, 65535>,
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().delay_ms)]
|
||||
pub delay_ms: u16,
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().max_speed)]
|
||||
pub max_speed: FloatOrInt<0, 1_000_000>,
|
||||
}
|
||||
|
||||
impl Default for DndEdgeWorkspaceSwitch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
trigger_height: FloatOrInt(50.),
|
||||
delay_ms: 100,
|
||||
max_speed: FloatOrInt(1500.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct HotCorners {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Overview {
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().zoom)]
|
||||
pub zoom: FloatOrInt<0, 1>,
|
||||
#[knuffel(child, default = Self::default().backdrop_color)]
|
||||
pub backdrop_color: Color,
|
||||
#[knuffel(child, default)]
|
||||
pub workspace_shadow: WorkspaceShadow,
|
||||
}
|
||||
|
||||
impl Default for Overview {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom: FloatOrInt(0.5),
|
||||
backdrop_color: DEFAULT_BACKDROP_COLOR,
|
||||
workspace_shadow: WorkspaceShadow::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
|
||||
|
||||
@@ -1311,6 +1457,8 @@ pub struct Match {
|
||||
#[knuffel(property)]
|
||||
pub is_window_cast_target: Option<bool>,
|
||||
#[knuffel(property)]
|
||||
pub is_urgent: Option<bool>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -1363,9 +1511,13 @@ pub struct BorderRule {
|
||||
#[knuffel(child)]
|
||||
pub inactive_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
|
||||
@@ -1395,9 +1547,13 @@ pub struct TabIndicatorRule {
|
||||
#[knuffel(child)]
|
||||
pub inactive_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub urgent_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
@@ -1540,7 +1696,11 @@ pub enum Action {
|
||||
FocusWindowInColumn(#[knuffel(argument)] u8),
|
||||
FocusWindowPrevious,
|
||||
FocusColumnLeft,
|
||||
#[knuffel(skip)]
|
||||
FocusColumnLeftUnderMouse,
|
||||
FocusColumnRight,
|
||||
#[knuffel(skip)]
|
||||
FocusColumnRightUnderMouse,
|
||||
FocusColumnFirst,
|
||||
FocusColumnLast,
|
||||
FocusColumnRightOrFirst,
|
||||
@@ -1589,8 +1749,13 @@ pub enum Action {
|
||||
CenterWindow,
|
||||
#[knuffel(skip)]
|
||||
CenterWindowById(u64),
|
||||
CenterVisibleColumns,
|
||||
FocusWorkspaceDown,
|
||||
#[knuffel(skip)]
|
||||
FocusWorkspaceDownUnderMouse,
|
||||
FocusWorkspaceUp,
|
||||
#[knuffel(skip)]
|
||||
FocusWorkspaceUpUnderMouse,
|
||||
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
|
||||
FocusWorkspacePrevious,
|
||||
MoveWindowToWorkspaceDown,
|
||||
@@ -1605,9 +1770,12 @@ pub enum Action {
|
||||
reference: WorkspaceReference,
|
||||
focus: bool,
|
||||
},
|
||||
MoveColumnToWorkspaceDown,
|
||||
MoveColumnToWorkspaceUp,
|
||||
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
|
||||
MoveColumnToWorkspaceDown(#[knuffel(property(name = "focus"), default = true)] bool),
|
||||
MoveColumnToWorkspaceUp(#[knuffel(property(name = "focus"), default = true)] bool),
|
||||
MoveColumnToWorkspace(
|
||||
#[knuffel(argument)] WorkspaceReference,
|
||||
#[knuffel(property(name = "focus"), default = true)] bool,
|
||||
),
|
||||
MoveWorkspaceDown,
|
||||
MoveWorkspaceUp,
|
||||
MoveWorkspaceToIndex(#[knuffel(argument)] usize),
|
||||
@@ -1716,6 +1884,15 @@ pub enum Action {
|
||||
SetDynamicCastWindowById(u64),
|
||||
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
|
||||
ClearDynamicCastTarget,
|
||||
ToggleOverview,
|
||||
OpenOverview,
|
||||
CloseOverview,
|
||||
#[knuffel(skip)]
|
||||
ToggleUrgent(u64),
|
||||
#[knuffel(skip)]
|
||||
SetUrgent(u64),
|
||||
#[knuffel(skip)]
|
||||
UnsetUrgent(u64),
|
||||
}
|
||||
|
||||
impl From<niri_ipc::Action> for Action {
|
||||
@@ -1816,6 +1993,7 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
|
||||
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
|
||||
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
|
||||
niri_ipc::Action::CenterVisibleColumns {} => Self::CenterVisibleColumns,
|
||||
niri_ipc::Action::FocusWorkspaceDown {} => Self::FocusWorkspaceDown,
|
||||
niri_ipc::Action::FocusWorkspaceUp {} => Self::FocusWorkspaceUp,
|
||||
niri_ipc::Action::FocusWorkspace { reference } => {
|
||||
@@ -1838,10 +2016,14 @@ impl From<niri_ipc::Action> for Action {
|
||||
reference: WorkspaceReference::from(reference),
|
||||
focus,
|
||||
},
|
||||
niri_ipc::Action::MoveColumnToWorkspaceDown {} => Self::MoveColumnToWorkspaceDown,
|
||||
niri_ipc::Action::MoveColumnToWorkspaceUp {} => Self::MoveColumnToWorkspaceUp,
|
||||
niri_ipc::Action::MoveColumnToWorkspace { reference } => {
|
||||
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference))
|
||||
niri_ipc::Action::MoveColumnToWorkspaceDown { focus } => {
|
||||
Self::MoveColumnToWorkspaceDown(focus)
|
||||
}
|
||||
niri_ipc::Action::MoveColumnToWorkspaceUp { focus } => {
|
||||
Self::MoveColumnToWorkspaceUp(focus)
|
||||
}
|
||||
niri_ipc::Action::MoveColumnToWorkspace { reference, focus } => {
|
||||
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference), focus)
|
||||
}
|
||||
niri_ipc::Action::MoveWorkspaceDown {} => Self::MoveWorkspaceDown,
|
||||
niri_ipc::Action::MoveWorkspaceUp {} => Self::MoveWorkspaceUp,
|
||||
@@ -1980,6 +2162,12 @@ impl From<niri_ipc::Action> for Action {
|
||||
Self::SetDynamicCastMonitor(output)
|
||||
}
|
||||
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
|
||||
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
|
||||
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
|
||||
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
|
||||
niri_ipc::Action::ToggleUrgent { id } => Self::ToggleUrgent(id),
|
||||
niri_ipc::Action::SetUrgent { id } => Self::SetUrgent(id),
|
||||
niri_ipc::Action::UnsetUrgent { id } => Self::UnsetUrgent(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2208,12 +2396,18 @@ impl BorderRule {
|
||||
if let Some(x) = other.inactive_color {
|
||||
self.inactive_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.urgent_color {
|
||||
self.urgent_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.active_gradient {
|
||||
self.active_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = other.inactive_gradient {
|
||||
self.inactive_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = other.urgent_gradient {
|
||||
self.urgent_gradient = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_against(&self, mut config: Border) -> Border {
|
||||
@@ -2233,12 +2427,19 @@ impl BorderRule {
|
||||
config.inactive_color = x;
|
||||
config.inactive_gradient = None;
|
||||
}
|
||||
if let Some(x) = self.urgent_color {
|
||||
config.urgent_color = x;
|
||||
config.urgent_gradient = None;
|
||||
}
|
||||
if let Some(x) = self.active_gradient {
|
||||
config.active_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = self.inactive_gradient {
|
||||
config.inactive_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = self.urgent_gradient {
|
||||
config.urgent_gradient = Some(x);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
@@ -2313,12 +2514,18 @@ impl TabIndicatorRule {
|
||||
if let Some(x) = other.inactive_color {
|
||||
self.inactive_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.urgent_color {
|
||||
self.urgent_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.active_gradient {
|
||||
self.active_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = other.inactive_gradient {
|
||||
self.inactive_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = other.urgent_gradient {
|
||||
self.urgent_gradient = Some(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2964,6 +3171,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> knuffel::Decode<S> for OverviewOpenCloseAnim
|
||||
where
|
||||
S: knuffel::traits::ErrorSpan,
|
||||
{
|
||||
fn decode_node(
|
||||
node: &knuffel::ast::SpannedNode<S>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
let default = Self::default().0;
|
||||
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
|
||||
Ok(false)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new_off() -> Self {
|
||||
Self {
|
||||
@@ -3958,6 +4180,7 @@ mod tests {
|
||||
repeat_delay: 600,
|
||||
repeat_rate: 25,
|
||||
track_layout: Window,
|
||||
numlock: false,
|
||||
},
|
||||
touchpad: Touchpad {
|
||||
off: false,
|
||||
@@ -4121,12 +4344,15 @@ mod tests {
|
||||
},
|
||||
),
|
||||
focus_at_startup: true,
|
||||
background_color: Color {
|
||||
r: 0.09803922,
|
||||
g: 0.09803922,
|
||||
b: 0.4,
|
||||
a: 1.0,
|
||||
},
|
||||
background_color: Some(
|
||||
Color {
|
||||
r: 0.09803922,
|
||||
g: 0.09803922,
|
||||
b: 0.4,
|
||||
a: 1.0,
|
||||
},
|
||||
),
|
||||
backdrop_color: None,
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -4157,6 +4383,12 @@ mod tests {
|
||||
b: 0.39215687,
|
||||
a: 0.0,
|
||||
},
|
||||
urgent_color: Color {
|
||||
r: 0.60784316,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
active_gradient: Some(
|
||||
Gradient {
|
||||
from: Color {
|
||||
@@ -4180,6 +4412,7 @@ mod tests {
|
||||
},
|
||||
),
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
border: Border {
|
||||
off: false,
|
||||
@@ -4198,8 +4431,15 @@ mod tests {
|
||||
b: 0.39215687,
|
||||
a: 0.0,
|
||||
},
|
||||
urgent_color: Color {
|
||||
r: 0.60784316,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
shadow: Shadow {
|
||||
on: false,
|
||||
@@ -4250,8 +4490,10 @@ mod tests {
|
||||
),
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
insert_hint: InsertHint {
|
||||
off: false,
|
||||
@@ -4342,6 +4584,12 @@ mod tests {
|
||||
0.0,
|
||||
),
|
||||
},
|
||||
background_color: Color {
|
||||
r: 0.25,
|
||||
g: 0.25,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
},
|
||||
},
|
||||
prefer_no_csd: true,
|
||||
cursor: Cursor {
|
||||
@@ -4459,6 +4707,18 @@ mod tests {
|
||||
),
|
||||
},
|
||||
),
|
||||
overview_open_close: OverviewOpenCloseAnim(
|
||||
Animation {
|
||||
off: false,
|
||||
kind: Spring(
|
||||
SpringParams {
|
||||
damping_ratio: 1.0,
|
||||
stiffness: 800,
|
||||
epsilon: 0.0001,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
gestures: Gestures {
|
||||
dnd_edge_view_scroll: DndEdgeViewScroll {
|
||||
@@ -4470,6 +4730,52 @@ mod tests {
|
||||
50.0,
|
||||
),
|
||||
},
|
||||
dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch {
|
||||
trigger_height: FloatOrInt(
|
||||
50.0,
|
||||
),
|
||||
delay_ms: 100,
|
||||
max_speed: FloatOrInt(
|
||||
1500.0,
|
||||
),
|
||||
},
|
||||
hot_corners: HotCorners {
|
||||
off: false,
|
||||
},
|
||||
},
|
||||
overview: Overview {
|
||||
zoom: FloatOrInt(
|
||||
0.5,
|
||||
),
|
||||
backdrop_color: Color {
|
||||
r: 0.15,
|
||||
g: 0.15,
|
||||
b: 0.15,
|
||||
a: 1.0,
|
||||
},
|
||||
workspace_shadow: WorkspaceShadow {
|
||||
off: false,
|
||||
offset: ShadowOffset {
|
||||
x: FloatOrInt(
|
||||
0.0,
|
||||
),
|
||||
y: FloatOrInt(
|
||||
10.0,
|
||||
),
|
||||
},
|
||||
softness: FloatOrInt(
|
||||
40.0,
|
||||
),
|
||||
spread: FloatOrInt(
|
||||
10.0,
|
||||
),
|
||||
color: Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 0.3137255,
|
||||
},
|
||||
},
|
||||
},
|
||||
environment: Environment(
|
||||
[
|
||||
@@ -4502,6 +4808,7 @@ mod tests {
|
||||
is_active_in_column: None,
|
||||
is_floating: None,
|
||||
is_window_cast_target: None,
|
||||
is_urgent: None,
|
||||
at_startup: None,
|
||||
},
|
||||
],
|
||||
@@ -4520,6 +4827,7 @@ mod tests {
|
||||
is_active_in_column: None,
|
||||
is_floating: None,
|
||||
is_window_cast_target: None,
|
||||
is_urgent: None,
|
||||
at_startup: None,
|
||||
},
|
||||
Match {
|
||||
@@ -4534,6 +4842,7 @@ mod tests {
|
||||
is_active_in_column: None,
|
||||
is_floating: None,
|
||||
is_window_cast_target: None,
|
||||
is_urgent: None,
|
||||
at_startup: None,
|
||||
},
|
||||
],
|
||||
@@ -4577,8 +4886,10 @@ mod tests {
|
||||
),
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
border: BorderRule {
|
||||
off: false,
|
||||
@@ -4590,8 +4901,10 @@ mod tests {
|
||||
),
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
@@ -4613,8 +4926,10 @@ mod tests {
|
||||
},
|
||||
),
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
draw_border_with_background: None,
|
||||
opacity: None,
|
||||
@@ -4671,6 +4986,8 @@ mod tests {
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: None,
|
||||
baba_is_float: None,
|
||||
},
|
||||
],
|
||||
binds: Binds(
|
||||
@@ -5323,8 +5640,10 @@ mod tests {
|
||||
width: None,
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
};
|
||||
|
||||
for rule in rules.iter().copied() {
|
||||
|
||||
+1
-1
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.2.0"
|
||||
niri-ipc = "=25.5.0"
|
||||
```
|
||||
|
||||
+106
-7
@@ -1,8 +1,23 @@
|
||||
//! Types for communicating with niri via IPC.
|
||||
//!
|
||||
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
|
||||
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
|
||||
//! can keep reading [`Event`]s from the socket after the response.
|
||||
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
|
||||
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
|
||||
//! wrapping a [`Response`].
|
||||
//!
|
||||
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
|
||||
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
|
||||
//! event stream and write more requests at the same time, you need to use two IPC sockets.
|
||||
//!
|
||||
//! <div class="warning">
|
||||
//!
|
||||
//! Requests are *always* processed separately. Time passes between requests, even when sending
|
||||
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
|
||||
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
|
||||
//! new workspace in-between the two responses). This goes for actions too: sending
|
||||
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
|
||||
//! the wrong window because a different window got focused in-between these requests.
|
||||
//!
|
||||
//! </div>
|
||||
//!
|
||||
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
|
||||
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
|
||||
@@ -12,7 +27,9 @@
|
||||
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
|
||||
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
|
||||
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
|
||||
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
|
||||
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
|
||||
//! separate line.
|
||||
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
|
||||
//! on a single line each.
|
||||
//!
|
||||
//! ## Backwards compatibility
|
||||
@@ -24,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.2.0"
|
||||
//! niri-ipc = "=25.5.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -97,6 +114,8 @@ pub enum Request {
|
||||
EventStream,
|
||||
/// Respond with an error (for testing error handling).
|
||||
ReturnError,
|
||||
/// Request information about the overview.
|
||||
OverviewState,
|
||||
}
|
||||
|
||||
/// Reply from niri to client.
|
||||
@@ -139,6 +158,16 @@ pub enum Response {
|
||||
PickedColor(Option<PickedColor>),
|
||||
/// Output configuration change result.
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
/// Information about the overview.
|
||||
OverviewState(Overview),
|
||||
}
|
||||
|
||||
/// Overview information.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Overview {
|
||||
/// Whether the overview is currently open.
|
||||
pub is_open: bool,
|
||||
}
|
||||
|
||||
/// Color picked from the screen.
|
||||
@@ -397,6 +426,8 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Center all fully visible columns on the screen.
|
||||
CenterVisibleColumns {},
|
||||
/// Focus the workspace below.
|
||||
FocusWorkspaceDown {},
|
||||
/// Focus the workspace above.
|
||||
@@ -438,14 +469,35 @@ pub enum Action {
|
||||
focus: bool,
|
||||
},
|
||||
/// Move the focused column to the workspace below.
|
||||
MoveColumnToWorkspaceDown {},
|
||||
MoveColumnToWorkspaceDown {
|
||||
/// Whether the focus should follow the target workspace.
|
||||
///
|
||||
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||
/// `false`, the focus will remain on the original workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
focus: bool,
|
||||
},
|
||||
/// Move the focused column to the workspace above.
|
||||
MoveColumnToWorkspaceUp {},
|
||||
MoveColumnToWorkspaceUp {
|
||||
/// Whether the focus should follow the target workspace.
|
||||
///
|
||||
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||
/// `false`, the focus will remain on the original workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
focus: bool,
|
||||
},
|
||||
/// Move the focused column to a workspace by reference (index or name).
|
||||
MoveColumnToWorkspace {
|
||||
/// Reference (index or name) of the workspace to move the column to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
reference: WorkspaceReferenceArg,
|
||||
|
||||
/// Whether the focus should follow the target workspace.
|
||||
///
|
||||
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||
/// `false`, the focus will remain on the original workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
focus: bool,
|
||||
},
|
||||
/// Move the focused workspace down.
|
||||
MoveWorkspaceDown {},
|
||||
@@ -764,6 +816,30 @@ pub enum Action {
|
||||
},
|
||||
/// Clear the dynamic cast target, making it show nothing.
|
||||
ClearDynamicCastTarget {},
|
||||
/// Toggle (open/close) the Overview.
|
||||
ToggleOverview {},
|
||||
/// Open the Overview.
|
||||
OpenOverview {},
|
||||
/// Close the Overview.
|
||||
CloseOverview {},
|
||||
/// Toggle urgent status of a window.
|
||||
ToggleUrgent {
|
||||
/// Id of the window to toggle urgent.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
/// Set urgent status of a window.
|
||||
SetUrgent {
|
||||
/// Id of the window to set urgent.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
/// Unset urgent status of a window.
|
||||
UnsetUrgent {
|
||||
/// Id of the window to unset urgent.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
@@ -1072,6 +1148,8 @@ pub struct Window {
|
||||
///
|
||||
/// If the window isn't floating then it is in the tiling layout.
|
||||
pub is_floating: bool,
|
||||
/// Whether this window requests your attention.
|
||||
pub is_urgent: bool,
|
||||
}
|
||||
|
||||
/// Output configuration change result.
|
||||
@@ -1111,6 +1189,8 @@ pub struct Workspace {
|
||||
///
|
||||
/// Can be `None` if no outputs are currently connected.
|
||||
pub output: Option<String>,
|
||||
/// Whether the workspace currently has an urgent window in its output.
|
||||
pub is_urgent: bool,
|
||||
/// Whether the workspace is currently active on its output.
|
||||
///
|
||||
/// Every output has one active workspace, the one that is currently visible on that output.
|
||||
@@ -1185,6 +1265,13 @@ pub enum Event {
|
||||
/// workspaces are missing from here, then they were deleted.
|
||||
workspaces: Vec<Workspace>,
|
||||
},
|
||||
/// The workspace urgency changed.
|
||||
WorkspaceUrgencyChanged {
|
||||
/// Id of the workspace.
|
||||
id: u64,
|
||||
/// Whether this workspace has an urgent window.
|
||||
urgent: bool,
|
||||
},
|
||||
/// A workspace was activated on an output.
|
||||
///
|
||||
/// This doesn't always mean the workspace became focused, just that it's now the active
|
||||
@@ -1232,6 +1319,13 @@ pub enum Event {
|
||||
/// Id of the newly focused window, or `None` if no window is now focused.
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Window urgency changed.
|
||||
WindowUrgencyChanged {
|
||||
/// Id of the window.
|
||||
id: u64,
|
||||
/// The new urgency state of the window.
|
||||
urgent: bool,
|
||||
},
|
||||
/// The configured keyboard layouts have changed.
|
||||
KeyboardLayoutsChanged {
|
||||
/// The new keyboard layout configuration.
|
||||
@@ -1242,6 +1336,11 @@ pub enum Event {
|
||||
/// Index of the newly active layout.
|
||||
idx: u8,
|
||||
},
|
||||
/// The overview was opened or closed.
|
||||
OverviewOpenedOrClosed {
|
||||
/// The new state of the overview.
|
||||
is_open: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for WorkspaceReferenceArg {
|
||||
|
||||
+42
-18
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
||||
/// and serialization/deserialization of messages.
|
||||
pub struct Socket {
|
||||
stream: UnixStream,
|
||||
stream: BufReader<UnixStream>,
|
||||
}
|
||||
|
||||
impl Socket {
|
||||
@@ -37,6 +37,7 @@ impl Socket {
|
||||
/// Connects to the niri IPC socket at the given path.
|
||||
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let stream = UnixStream::connect(path.as_ref())?;
|
||||
let stream = BufReader::new(stream);
|
||||
Ok(Self { stream })
|
||||
}
|
||||
|
||||
@@ -47,31 +48,54 @@ impl Socket {
|
||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||
/// * `Ok(Err(message))`: error message from niri
|
||||
/// * `Err(error)`: error communicating with niri
|
||||
///
|
||||
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
|
||||
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
|
||||
/// otherwise.
|
||||
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
|
||||
let Self { mut stream } = self;
|
||||
|
||||
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
stream.write_all(buf.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
buf.push('\n');
|
||||
self.stream.get_mut().write_all(buf.as_bytes())?;
|
||||
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
self.stream.read_line(&mut buf)?;
|
||||
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
let events = move || {
|
||||
/// Starts reading event stream [`Event`]s from the socket.
|
||||
///
|
||||
/// The returned function will block until the next [`Event`] arrives, then return it.
|
||||
///
|
||||
/// Use this only after requesting an [`EventStream`][Request::EventStream].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use niri_ipc::{Request, Response};
|
||||
/// use niri_ipc::socket::Socket;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let mut socket = Socket::connect()?;
|
||||
///
|
||||
/// let reply = socket.send(Request::EventStream)?;
|
||||
/// if matches!(reply, Ok(Response::Handled)) {
|
||||
/// let mut read_event = socket.read_events();
|
||||
/// while let Ok(event) = read_event() {
|
||||
/// println!("Received event: {event:?}");
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
|
||||
let Self { mut stream } = self;
|
||||
let _ = stream.get_mut().shutdown(Shutdown::Write);
|
||||
|
||||
let mut buf = String::new();
|
||||
move || {
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
stream.read_line(&mut buf)?;
|
||||
let event = serde_json::from_str(&buf)?;
|
||||
Ok(event)
|
||||
};
|
||||
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ pub struct EventStreamState {
|
||||
|
||||
/// State of the keyboard layouts.
|
||||
pub keyboard_layouts: KeyboardLayoutsState,
|
||||
|
||||
/// State of the overview.
|
||||
pub overview: OverviewState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
@@ -63,12 +66,20 @@ pub struct KeyboardLayoutsState {
|
||||
pub keyboard_layouts: Option<KeyboardLayouts>,
|
||||
}
|
||||
|
||||
/// The overview state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OverviewState {
|
||||
/// Whether the overview is currently open.
|
||||
pub is_open: bool,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.workspaces.replicate());
|
||||
events.extend(self.windows.replicate());
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events.extend(self.overview.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
@@ -76,6 +87,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
let event = self.workspaces.apply(event)?;
|
||||
let event = self.windows.apply(event)?;
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
let event = self.overview.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +103,13 @@ impl EventStreamStatePart for WorkspacesState {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
||||
}
|
||||
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||
for ws in self.workspaces.values_mut() {
|
||||
if ws.id == id {
|
||||
ws.is_urgent = urgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let ws = self.workspaces.get(&id);
|
||||
let ws = ws.expect("activated workspace was missing from the map");
|
||||
@@ -162,6 +181,14 @@ impl EventStreamStatePart for WindowsState {
|
||||
win.is_focused = Some(win.id) == id;
|
||||
}
|
||||
}
|
||||
Event::WindowUrgencyChanged { id, urgent } => {
|
||||
for win in self.windows.values_mut() {
|
||||
if win.id == id {
|
||||
win.is_urgent = urgent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
@@ -192,3 +219,21 @@ impl EventStreamStatePart for KeyboardLayoutsState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for OverviewState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
vec![Event::OverviewOpenedOrClosed {
|
||||
is_open: self.is_open,
|
||||
}]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::OverviewOpenedOrClosed { is_open } => {
|
||||
self.is_open = is_open;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ repository.workspace = true
|
||||
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.2.0", path = ".." }
|
||||
niri-config = { version = "25.2.0", path = "../niri-config" }
|
||||
niri = { version = "25.5.0", path = ".." }
|
||||
niri-config = { version = "25.5.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -23,8 +23,10 @@ impl GradientArea {
|
||||
width: FloatOrInt(1.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
||||
inactive_color: Color::default(),
|
||||
urgent_color: Color::default(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -81,6 +83,7 @@ impl TestCase for GradientArea {
|
||||
g_size,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Rectangle::default(),
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
|
||||
@@ -60,8 +60,10 @@ impl Layout {
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
||||
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
@@ -266,6 +268,7 @@ impl TestCase for Layout {
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -272,4 +272,8 @@ impl LayoutElement for TestWindow {
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_urgent(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This config is in the KDL format: https://kdl.dev
|
||||
// "/-" comments out the following node.
|
||||
// Check the wiki for a full description of the configuration:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
@@ -16,6 +16,9 @@ input {
|
||||
// layout "us,ru"
|
||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||
}
|
||||
|
||||
// Enable numlock on startup, omitting this setting disables it.
|
||||
numlock
|
||||
}
|
||||
|
||||
// Next sections include libinput settings.
|
||||
@@ -189,6 +192,9 @@ layout {
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
|
||||
// Color of the border around windows that request your attention.
|
||||
urgent-color "#9b0000"
|
||||
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
@@ -350,6 +356,11 @@ binds {
|
||||
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
||||
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||
|
||||
// Open/close the Overview: a zoomed-out view of workspaces and windows.
|
||||
// You can also move the mouse into the top-left hot corner,
|
||||
// or do a four-finger swipe up on a touchpad.
|
||||
Mod+O repeat=false { toggle-overview; }
|
||||
|
||||
Mod+Q { close-window; }
|
||||
|
||||
Mod+Left { focus-column-left; }
|
||||
@@ -514,6 +525,9 @@ binds {
|
||||
|
||||
Mod+C { center-column; }
|
||||
|
||||
// Center all fully visible columns on screen.
|
||||
Mod+Ctrl+C { center-visible-columns; }
|
||||
|
||||
// Finer width adjustments.
|
||||
// This command can also:
|
||||
// * set width in pixels: "1000"
|
||||
|
||||
@@ -94,6 +94,12 @@ impl Spring {
|
||||
|
||||
x1 = (self.to - y0 + m * x0) / m;
|
||||
y1 = self.oscillate(x1);
|
||||
|
||||
// Overdamped springs have some numerical stability issues...
|
||||
if !y1.is_finite() {
|
||||
return Duration::from_secs_f64(x0);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@@ -187,4 +193,17 @@ mod tests {
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_duration_panic() {
|
||||
let spring = Spring {
|
||||
from: 0.,
|
||||
to: 1.,
|
||||
initial_velocity: 0.,
|
||||
params: SpringParams::new(6., 1200., 0.0001),
|
||||
};
|
||||
let _ = spring.duration();
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub use winit::Winit;
|
||||
pub mod headless;
|
||||
pub use headless::Headless;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Backend {
|
||||
Tty(Tty),
|
||||
Winit(Winit),
|
||||
|
||||
+4
-3
@@ -19,6 +19,7 @@ use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
||||
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||
use smithay::backend::drm::{
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
|
||||
};
|
||||
@@ -114,7 +115,7 @@ pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Er
|
||||
|
||||
type GbmDrmCompositor = DrmCompositor<
|
||||
GbmAllocator<DrmDeviceFd>,
|
||||
GbmDevice<DrmDeviceFd>,
|
||||
GbmFramebufferExporter<DrmDeviceFd>,
|
||||
(OutputPresentationFeedback, Duration),
|
||||
DrmDeviceFd,
|
||||
>;
|
||||
@@ -971,7 +972,7 @@ impl Tty {
|
||||
surface,
|
||||
None,
|
||||
allocator.clone(),
|
||||
device.gbm.clone(),
|
||||
GbmFramebufferExporter::new(device.gbm.clone()),
|
||||
SUPPORTED_COLOR_FORMATS,
|
||||
// This is only used to pick a good internal format, so it can use the surface's render
|
||||
// formats, even though we only ever render on the primary GPU.
|
||||
@@ -1001,7 +1002,7 @@ impl Tty {
|
||||
surface,
|
||||
None,
|
||||
allocator,
|
||||
device.gbm.clone(),
|
||||
GbmFramebufferExporter::new(device.gbm.clone()),
|
||||
SUPPORTED_COLOR_FORMATS,
|
||||
render_formats,
|
||||
device.drm.cursor_size(),
|
||||
|
||||
@@ -105,4 +105,6 @@ pub enum Msg {
|
||||
Version,
|
||||
/// Request an error from the running niri instance.
|
||||
RequestError,
|
||||
/// Print the overview state.
|
||||
OverviewState,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||
use crate::niri::State;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
use crate::utils::{is_mapped, output_size, send_scale_transform};
|
||||
|
||||
impl WlrLayerShellHandler for State {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
@@ -125,10 +125,23 @@ impl State {
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let config = self.niri.config.borrow();
|
||||
|
||||
let rules = &config.layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules, &config);
|
||||
|
||||
let output_size = output_size(&output);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
let mapped = MappedLayer::new(
|
||||
layer.clone(),
|
||||
rules,
|
||||
output_size,
|
||||
scale,
|
||||
self.niri.clock.clone(),
|
||||
&config,
|
||||
);
|
||||
|
||||
let prev = self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
|
||||
+15
-9
@@ -211,7 +211,7 @@ impl PointerConstraintsHandler for State {
|
||||
pointer.set_location(target);
|
||||
|
||||
// Redraw to update the cursor position if it's visible.
|
||||
if !self.niri.pointer_hidden {
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
@@ -369,7 +369,7 @@ impl ClientDndGrabHandler for State {
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if !self.niri.pointer_hidden {
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
@@ -707,6 +707,8 @@ impl GammaControlHandler for State {
|
||||
}
|
||||
delegate_gamma_control!(State);
|
||||
|
||||
struct UrgentOnlyMarker;
|
||||
|
||||
impl XdgActivationHandler for State {
|
||||
fn activation_state(&mut self) -> &mut XdgActivationState {
|
||||
&mut self.niri.activation_state
|
||||
@@ -716,11 +718,10 @@ impl XdgActivationHandler for State {
|
||||
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
|
||||
// common client behavior.
|
||||
//
|
||||
// We don't have urgency yet, so just ignore such tokens.
|
||||
//
|
||||
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
|
||||
let Some((serial, seat)) = data.serial else {
|
||||
return false;
|
||||
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
|
||||
return true;
|
||||
};
|
||||
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
||||
return false;
|
||||
@@ -760,11 +761,16 @@ impl XdgActivationHandler for State {
|
||||
surface: WlSurface,
|
||||
) {
|
||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&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();
|
||||
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
|
||||
mapped.set_urgent(true);
|
||||
self.niri.queue_redraw_all();
|
||||
} else {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
|
||||
unmapped.activation_token_data = Some(token_data);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = MoveGrab::new(start_data, window);
|
||||
let grab = MoveGrab::new(start_data, window, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
@@ -316,10 +316,20 @@ impl XdgShellHandler for State {
|
||||
} else if let Some(output) = self.niri.layout.active_output() {
|
||||
let layers = layer_map_for_output(output);
|
||||
|
||||
if layers
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.is_none()
|
||||
{
|
||||
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
|
||||
// in update_keyboard_focus().
|
||||
|
||||
if let Some(layer) = layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) {
|
||||
// This is a grab for a layer surface.
|
||||
|
||||
if let Some(mapped) = self.niri.mapped_layer_surfaces.get(layer) {
|
||||
if mapped.place_within_backdrop() {
|
||||
trace!("ignoring popup grab for a layer surface within overview backdrop");
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a grab for a regular window; check that there's no layer surface with a
|
||||
// higher input priority.
|
||||
|
||||
@@ -1062,6 +1072,19 @@ impl State {
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
let mut target = Rectangle::from_size(output_geo.size);
|
||||
|
||||
// Background and bottom layer popups render below the top and the overlay layer, so let's
|
||||
// put them into the non-exclusive zone.
|
||||
//
|
||||
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
|
||||
// Smithay only computes the "all layers" non-exclusive zone atm.
|
||||
//
|
||||
// FIXME: related to the above, top layer popups should use the "overlay layer"
|
||||
// non-exclusive zone.
|
||||
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
|
||||
target = map.non_exclusive_zone();
|
||||
}
|
||||
|
||||
target.loc -= layer_geo.loc;
|
||||
target.loc -= get_popup_toplevel_coords(popup);
|
||||
|
||||
|
||||
+781
-149
File diff suppressed because it is too large
Load Diff
+42
-5
@@ -1,10 +1,11 @@
|
||||
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,
|
||||
AxisFrame, ButtonEvent, CursorIcon, 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};
|
||||
@@ -15,14 +16,32 @@ pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
Move,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
use_threshold: bool,
|
||||
) -> Self {
|
||||
let gesture = if use_threshold {
|
||||
GestureState::Recognizing
|
||||
} else {
|
||||
GestureState::Move
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
self.gesture = GestureState::Move;
|
||||
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
|
||||
if self.gesture != GestureState::Move {
|
||||
return;
|
||||
}
|
||||
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Swipe gesture from scroll events.
|
||||
//!
|
||||
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
|
||||
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
|
||||
//! gesture events for 3+ fingers.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollSwipeGesture {
|
||||
ongoing: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
BeginUpdate,
|
||||
Update,
|
||||
End,
|
||||
}
|
||||
|
||||
impl ScrollSwipeGesture {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
ongoing: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
|
||||
if dx == 0. && dy == 0. {
|
||||
self.ongoing = false;
|
||||
Action::End
|
||||
} else if !self.ongoing {
|
||||
self.ongoing = true;
|
||||
self.vertical = dy != 0.;
|
||||
Action::BeginUpdate
|
||||
} else {
|
||||
Action::Update
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> bool {
|
||||
if self.ongoing {
|
||||
self.ongoing = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
self.vertical
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScrollSwipeGesture {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn begin(self) -> bool {
|
||||
self == Action::BeginUpdate
|
||||
}
|
||||
|
||||
pub fn end(self) -> bool {
|
||||
self == Action::End
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
@@ -27,12 +29,24 @@ enum GestureState {
|
||||
}
|
||||
|
||||
impl SpatialMovementGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
GestureState::Recognizing
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
gesture: GestureState::Recognizing,
|
||||
workspace_id,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +54,8 @@ impl SpatialMovementGrab {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_end(false, Some(false))
|
||||
}
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
@@ -81,8 +93,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
if c.x.abs() > c.y.abs() {
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
layout.view_offset_gesture_begin(&self.output, false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::layout::workspace::{Workspace, WorkspaceId};
|
||||
use crate::niri::State;
|
||||
use crate::window::Mapped;
|
||||
|
||||
// When the touch is stationary for this much time, it becomes an interactive move.
|
||||
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
|
||||
|
||||
pub struct TouchOverviewGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
start_timestamp: Duration,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
start_pos_within_output: Point<f64, Logical>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
workspace_matched_narrow: bool,
|
||||
window: Option<Window>,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
WorkspaceSwitch,
|
||||
InteractiveMove,
|
||||
}
|
||||
|
||||
impl TouchOverviewGrab {
|
||||
pub fn new(
|
||||
start_data: TouchGrabStartData<State>,
|
||||
start_timestamp: Duration,
|
||||
output: Output,
|
||||
start_pos_within_output: Point<f64, Logical>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
workspace_matched_narrow: bool,
|
||||
window: Option<Window>,
|
||||
) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_timestamp,
|
||||
start_data,
|
||||
output,
|
||||
start_pos_within_output,
|
||||
workspace_id,
|
||||
workspace_matched_narrow,
|
||||
window,
|
||||
gesture: GestureState::Recognizing,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
// Tap to activate.
|
||||
layout.focus_output(&self.output);
|
||||
|
||||
// Activate the workspace if necessary.
|
||||
if self.window.is_some() || self.workspace_matched_narrow {
|
||||
// When activating a window, we want to activate the window's current
|
||||
// workspace. Otherwise, find the workspace that we tapped on.
|
||||
let ws_matches = |ws: &Workspace<Mapped>| {
|
||||
if let Some(window) = &self.window {
|
||||
ws.has_window(window)
|
||||
} else if let Some(ws_id) = self.workspace_id {
|
||||
ws.id() == ws_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
|
||||
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
|
||||
{
|
||||
// The workspace could've moved to a different output in the meantime.
|
||||
(*mon.output() == self.output).then_some(ws_idx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ws_idx) = ws_idx {
|
||||
layout.toggle_overview_to_workspace(ws_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
layout.activate_window(window);
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
layout.view_offset_gesture_end(Some(false));
|
||||
}
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_end(Some(false));
|
||||
}
|
||||
GestureState::InteractiveMove => {
|
||||
layout.interactive_move_end(self.window.as_ref().unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchOverviewGrab {
|
||||
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;
|
||||
}
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let layout = &mut data.niri.layout;
|
||||
|
||||
// Check if we should become interactive move.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
|
||||
let passed = timestamp.saturating_sub(self.start_timestamp);
|
||||
if INTERACTIVE_MOVE_THRESHOLD <= passed
|
||||
&& layout.interactive_move_begin(
|
||||
window.clone(),
|
||||
&self.output,
|
||||
self.start_pos_within_output,
|
||||
)
|
||||
{
|
||||
self.gesture = GestureState::InteractiveMove;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should become a spatial scroll.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
|
||||
if c.x * c.x + c.y * c.y >= 16. * 16. {
|
||||
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do nothing if still recognizing.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
let ongoing = match self.gesture {
|
||||
GestureState::Recognizing => unreachable!(),
|
||||
GestureState::ViewOffset => layout
|
||||
.view_offset_gesture_update(-delta.x, timestamp, false)
|
||||
.is_some(),
|
||||
GestureState::WorkspaceSwitch => layout
|
||||
.workspace_switch_gesture_update(-delta.y, timestamp, false)
|
||||
.is_some(),
|
||||
GestureState::InteractiveMove => {
|
||||
let window = self.window.as_ref().unwrap();
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
data.niri.layout.interactive_move_update(
|
||||
window,
|
||||
delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ongoing {
|
||||
data.niri.queue_redraw_all();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+78
-37
@@ -1,3 +1,4 @@
|
||||
use std::io::ErrorKind;
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
@@ -5,8 +6,8 @@ use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
|
||||
Transform, Window,
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
|
||||
Response, Transform, Window,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -32,24 +33,35 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
Msg::OverviewState => Request::OverviewState,
|
||||
};
|
||||
|
||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
|
||||
let (reply, mut read_event) = socket
|
||||
.send(request)
|
||||
.context("error communicating with niri")?;
|
||||
let result = socket.send(request);
|
||||
|
||||
let compositor_version = match reply {
|
||||
Err(_) if !matches!(msg, Msg::Version) => {
|
||||
// If we got an error, it might be that the CLI is a different version from the running
|
||||
// niri instance. Request the running instance version to compare and print a message.
|
||||
Socket::connect()
|
||||
.and_then(|socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
.map(|(reply, _read_event)| reply)
|
||||
// For errors that can be caused by a version mismatch between the running niri instance and
|
||||
// the niri msg CLI, we will try to fetch and compare the versions.
|
||||
let check_compositor_version = match &result {
|
||||
Err(err) => {
|
||||
// Response JSON parsing errors.
|
||||
matches!(
|
||||
err.kind(),
|
||||
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
// Error returned from niri.
|
||||
Ok(Err(_)) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
|
||||
// Reconnect to support older niri versions with one request per connection.
|
||||
Socket::connect()
|
||||
.and_then(|mut socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||
@@ -57,32 +69,31 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let response = reply.map_err(|err_msg| {
|
||||
// Check for CLI-server version mismatch to add helpful context.
|
||||
match compositor_version {
|
||||
Some(Ok(Response::Version(compositor_version))) => {
|
||||
let cli_version = version();
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||
eprintln!("Compositor version: {compositor_version}");
|
||||
eprintln!("CLI version: {cli_version}");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!("Unable to get the running niri compositor version.");
|
||||
// Check for CLI-server version mismatch to add helpful context.
|
||||
match compositor_version {
|
||||
Some(Ok(Response::Version(compositor_version))) => {
|
||||
let cli_version = version();
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||
eprintln!("Compositor version: {compositor_version}");
|
||||
eprintln!("CLI version: {cli_version}");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
None => {
|
||||
// Communication error, or the original request was already a version request.
|
||||
// Don't add irrelevant context.
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!("Unable to get the running niri compositor version.");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
None => {
|
||||
// Communication error, or the original request was already a version request, or the
|
||||
// original request had succeeded. Don't add irrelevant context.
|
||||
}
|
||||
}
|
||||
|
||||
anyhow!(err_msg).context("niri returned an error")
|
||||
})?;
|
||||
let reply = result.context("error communicating with niri")?;
|
||||
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
|
||||
|
||||
match msg {
|
||||
Msg::RequestError => {
|
||||
@@ -391,6 +402,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("Started reading events.");
|
||||
}
|
||||
|
||||
let mut read_event = socket.read_events();
|
||||
loop {
|
||||
let event = read_event().context("error reading event from niri")?;
|
||||
|
||||
@@ -404,6 +416,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
println!("Workspaces changed: {workspaces:?}");
|
||||
}
|
||||
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||
println!("Workspace {id}: urgency changed to {urgent}");
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let word = if focused { "focused" } else { "activated" };
|
||||
println!("Workspace {word}: {id}");
|
||||
@@ -429,15 +444,40 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Event::WindowFocusChanged { id } => {
|
||||
println!("Window focus changed: {id:?}");
|
||||
}
|
||||
Event::WindowUrgencyChanged { id, urgent } => {
|
||||
println!("Window {id}: urgency changed to {urgent}");
|
||||
}
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
println!("Keyboard layout switched: {idx}");
|
||||
}
|
||||
Event::OverviewOpenedOrClosed { is_open: opened } => {
|
||||
println!("Overview toggled: {opened}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::OverviewState => {
|
||||
let Response::OverviewState(response) = response else {
|
||||
bail!("unexpected response: expected Overview, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Overview { is_open } = response;
|
||||
if is_open {
|
||||
println!("Overview is open.");
|
||||
} else {
|
||||
println!("Overview is closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -541,7 +581,8 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
|
||||
fn print_window(window: &Window) {
|
||||
let focused = if window.is_focused { " (focused)" } else { "" };
|
||||
println!("Window ID {}:{focused}", window.id);
|
||||
let urgent = if window.is_urgent { " (urgent)" } else { "" };
|
||||
println!("Window ID {}:{focused}{urgent}", window.id);
|
||||
|
||||
if let Some(title) = &window.title {
|
||||
println!(" Title: \"{title}\"");
|
||||
|
||||
+113
-64
@@ -16,7 +16,9 @@ use futures_util::io::{AsyncReadExt, BufReader};
|
||||
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use niri_ipc::{
|
||||
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
|
||||
};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
|
||||
@@ -183,76 +185,86 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
||||
let (read, mut write) = stream.split();
|
||||
let mut buf = String::new();
|
||||
let mut read = BufReader::new(read);
|
||||
|
||||
// Read a single line to allow extensibility in the future to keep reading.
|
||||
BufReader::new(read)
|
||||
.read_line(&mut buf)
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
|
||||
let request = serde_json::from_str(&buf)
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
if let Err(err) = &reply {
|
||||
if !requested_error {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
buf.push(b'\n');
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
if requested_event_stream {
|
||||
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||
|
||||
// Spawn a task for the client.
|
||||
let client = EventStreamClient {
|
||||
events: events_rx,
|
||||
disconnect: disconnect_rx,
|
||||
write: Box::new(write) as _,
|
||||
};
|
||||
let future = async move {
|
||||
if let Err(err) = handle_event_stream_client(client).await {
|
||||
warn!("error handling IPC event stream client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC event stream future: {err:?}");
|
||||
}
|
||||
|
||||
// Send the initial state.
|
||||
{
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
for event in state.replicate() {
|
||||
events_tx
|
||||
.try_send(event)
|
||||
.expect("initial event burst had more events than buffer size");
|
||||
loop {
|
||||
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
|
||||
let mut buf = Vec::new();
|
||||
let res = read.read_until(b'\n', &mut buf).await;
|
||||
match res {
|
||||
Ok(0) => return Ok(()),
|
||||
Ok(_) => (),
|
||||
// Normal client disconnection.
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||
Err(err) => {
|
||||
return Err(err).context("error reading request");
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the list.
|
||||
{
|
||||
let mut streams = ctx.event_streams.borrow_mut();
|
||||
let sender = EventStreamSender {
|
||||
events: events_tx,
|
||||
disconnect: disconnect_tx,
|
||||
let request = serde_json::from_slice(&buf)
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
if let Err(err) = &reply {
|
||||
if !requested_error {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
serde_json::to_writer(&mut buf, &reply).context("error formatting reply")?;
|
||||
buf.push(b'\n');
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
if requested_event_stream {
|
||||
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||
|
||||
// Spawn a task for the client.
|
||||
let client = EventStreamClient {
|
||||
events: events_rx,
|
||||
disconnect: disconnect_rx,
|
||||
write: Box::new(write) as _,
|
||||
};
|
||||
streams.push(sender);
|
||||
let future = async move {
|
||||
if let Err(err) = handle_event_stream_client(client).await {
|
||||
warn!("error handling IPC event stream client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC event stream future: {err:?}");
|
||||
}
|
||||
|
||||
// Send the initial state.
|
||||
{
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
for event in state.replicate() {
|
||||
events_tx
|
||||
.try_send(event)
|
||||
.expect("initial event burst had more events than buffer size");
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the list.
|
||||
{
|
||||
let mut streams = ctx.event_streams.borrow_mut();
|
||||
let sender = EventStreamSender {
|
||||
events: events_tx,
|
||||
disconnect: disconnect_tx,
|
||||
};
|
||||
streams.push(sender);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
@@ -428,6 +440,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Response::FocusedOutput(output)
|
||||
}
|
||||
Request::EventStream => Response::Handled,
|
||||
Request::OverviewState => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let is_open = state.overview.is_open;
|
||||
Response::OverviewState(Overview { is_open })
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
@@ -469,6 +486,7 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
is_floating: mapped.is_floating(),
|
||||
is_urgent: mapped.is_urgent(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -524,6 +542,7 @@ impl State {
|
||||
pub fn ipc_refresh_layout(&mut self) {
|
||||
self.ipc_refresh_workspaces();
|
||||
self.ipc_refresh_windows();
|
||||
self.ipc_refresh_overview();
|
||||
}
|
||||
|
||||
fn ipc_refresh_workspaces(&mut self) {
|
||||
@@ -571,6 +590,12 @@ impl State {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this workspace urgent state changed.
|
||||
let urgent = ws.is_urgent();
|
||||
if urgent != ipc_ws.is_urgent {
|
||||
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
|
||||
}
|
||||
|
||||
// Check if this workspace became focused.
|
||||
let is_focused = Some(id) == focused_ws_id;
|
||||
if is_focused && !ipc_ws.is_focused {
|
||||
@@ -602,6 +627,7 @@ impl State {
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name().cloned(),
|
||||
output: mon.map(|mon| mon.output_name().clone()),
|
||||
is_urgent: ws.is_urgent(),
|
||||
is_active: mon.is_some_and(|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()),
|
||||
@@ -665,6 +691,11 @@ impl State {
|
||||
if mapped.is_focused() && !ipc_win.is_focused {
|
||||
events.push(Event::WindowFocusChanged { id: Some(id) });
|
||||
}
|
||||
|
||||
let urgent = mapped.is_urgent();
|
||||
if urgent != ipc_win.is_urgent {
|
||||
events.push(Event::WindowUrgencyChanged { id, urgent })
|
||||
}
|
||||
});
|
||||
|
||||
// Check for closed windows.
|
||||
@@ -690,4 +721,22 @@ impl State {
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_overview(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.overview;
|
||||
let is_open = self.niri.layout.is_overview_open();
|
||||
|
||||
if state.is_open == is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = Event::OverviewOpenedOrClosed { is_open };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-5
@@ -6,14 +6,17 @@ use smithay::backend::renderer::element::surface::{
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::animation::Clock;
|
||||
use crate::layout::shadow::Shadow;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MappedLayer {
|
||||
@@ -28,6 +31,15 @@ pub struct MappedLayer {
|
||||
|
||||
/// The shadow around the surface.
|
||||
shadow: Shadow,
|
||||
|
||||
/// The view size for the layer surface's output.
|
||||
view_size: Size<f64, Logical>,
|
||||
|
||||
/// Scale of the output the layer surface is on (and rounds its sizes to).
|
||||
scale: f64,
|
||||
|
||||
/// Clock for driving animations.
|
||||
clock: Clock,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
@@ -39,7 +51,14 @@ niri_render_elements! {
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules, config: &Config) -> Self {
|
||||
pub fn new(
|
||||
surface: LayerSurface,
|
||||
rules: ResolvedLayerRules,
|
||||
view_size: Size<f64, Logical>,
|
||||
scale: f64,
|
||||
clock: Clock,
|
||||
config: &Config,
|
||||
) -> Self {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
@@ -49,7 +68,10 @@ impl MappedLayer {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
view_size,
|
||||
scale,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
clock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,16 +87,27 @@ impl MappedLayer {
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, size: Size<f64, Logical>, scale: Scale<f64>) {
|
||||
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
|
||||
self.view_size = view_size;
|
||||
self.scale = scale;
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
|
||||
// Round to physical pixels.
|
||||
let size = size.to_physical_precise_round(scale).to_logical(scale);
|
||||
let size = size
|
||||
.to_physical_precise_round(self.scale)
|
||||
.to_logical(self.scale);
|
||||
|
||||
self.block_out_buffer.resize(size);
|
||||
|
||||
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||
// FIXME: is_active based on keyboard focus?
|
||||
self.shadow
|
||||
.update_render_elements(size, true, radius, scale.x, 1.);
|
||||
.update_render_elements(size, true, radius, self.scale, 1.);
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.rules.baba_is_float
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &LayerSurface {
|
||||
@@ -96,16 +129,44 @@ impl MappedLayer {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn place_within_backdrop(&self) -> bool {
|
||||
if !self.rules.place_within_backdrop {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.surface.layer() != Layer::Background {
|
||||
return false;
|
||||
}
|
||||
|
||||
let state = self.surface.cached_state();
|
||||
if state.exclusive_zone != ExclusiveZone::DontCare {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn bob_offset(&self) -> Point<f64, Logical> {
|
||||
if !self.rules.baba_is_float {
|
||||
return Point::from((0., 0.));
|
||||
}
|
||||
|
||||
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
|
||||
let y = round_logical_in_physical(self.scale, y);
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
let location = location + self.bob_offset();
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
|
||||
@@ -19,6 +19,12 @@ pub struct ResolvedLayerRules {
|
||||
|
||||
/// Corner radius to assume this layer surface has.
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
|
||||
/// Whether to place this layer surface within the overview backdrop.
|
||||
pub place_within_backdrop: bool,
|
||||
|
||||
/// Whether to bob this window up and down.
|
||||
pub baba_is_float: bool,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
@@ -37,6 +43,8 @@ impl ResolvedLayerRules {
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: false,
|
||||
baba_is_float: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +81,12 @@ impl ResolvedLayerRules {
|
||||
if let Some(x) = rule.geometry_corner_radius {
|
||||
resolved.geometry_corner_radius = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.place_within_backdrop {
|
||||
resolved.place_within_backdrop = x;
|
||||
}
|
||||
if let Some(x) = rule.baba_is_float {
|
||||
resolved.baba_is_float = x;
|
||||
}
|
||||
|
||||
resolved.shadow.merge_with(&rule.shadow);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ impl FocusRing {
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
is_border: bool,
|
||||
is_urgent: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
@@ -67,7 +68,9 @@ impl FocusRing {
|
||||
let width = self.config.width.0;
|
||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
let color = if is_urgent {
|
||||
self.config.urgent_color
|
||||
} else if is_active {
|
||||
self.config.active_color
|
||||
} else {
|
||||
self.config.inactive_color
|
||||
@@ -79,7 +82,9 @@ impl FocusRing {
|
||||
|
||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||
|
||||
let gradient = if is_active {
|
||||
let gradient = if is_urgent {
|
||||
self.config.urgent_gradient
|
||||
} else if is_active {
|
||||
self.config.active_gradient
|
||||
} else {
|
||||
self.config.inactive_gradient
|
||||
|
||||
@@ -19,8 +19,10 @@ impl InsertHintElement {
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
urgent_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
urgent_gradient: config.gradient,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -31,8 +33,10 @@ impl InsertHintElement {
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
urgent_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
urgent_gradient: config.gradient,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +52,7 @@ impl InsertHintElement {
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale, 1.);
|
||||
.update_render_elements(size, true, false, false, view_rect, radius, scale, 1.);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
|
||||
+766
-175
File diff suppressed because it is too large
Load Diff
+977
-200
File diff suppressed because it is too large
Load Diff
+158
-111
@@ -3,14 +3,14 @@ use std::iter::{self, zip};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, CornerRadius, PresetSize, Struts};
|
||||
use niri_config::{CenterFocusedColumn, PresetSize, Struts};
|
||||
use niri_ipc::{ColumnDisplay, SizeChange};
|
||||
use ordered_float::NotNan;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size};
|
||||
|
||||
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
|
||||
use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement};
|
||||
use super::monitor::InsertPosition;
|
||||
use super::tab_indicator::{TabIndicator, TabIndicatorRenderElement, TabInfo};
|
||||
use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
|
||||
use super::workspace::{InteractiveResize, ResolvedSize};
|
||||
@@ -67,12 +67,6 @@ pub struct ScrollingSpace<W: LayoutElement> {
|
||||
/// Windows in the closing animation.
|
||||
closing_windows: Vec<ClosingWindow>,
|
||||
|
||||
/// Indication where an interactively-moved window is about to be placed.
|
||||
insert_hint: Option<InsertHint>,
|
||||
|
||||
/// Insert hint element for rendering.
|
||||
insert_hint_element: InsertHintElement,
|
||||
|
||||
/// View size for this space.
|
||||
view_size: Size<f64, Logical>,
|
||||
|
||||
@@ -96,23 +90,9 @@ niri_render_elements! {
|
||||
Tile = TileRenderElement<R>,
|
||||
ClosingWindow = ClosingWindowRenderElement,
|
||||
TabIndicator = TabIndicatorRenderElement,
|
||||
InsertHint = InsertHintRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InsertPosition {
|
||||
NewColumn(usize),
|
||||
InColumn(usize, usize),
|
||||
Floating,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertHint {
|
||||
pub position: InsertPosition,
|
||||
pub corner_radius: CornerRadius,
|
||||
}
|
||||
|
||||
/// Extra per-column data.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct ColumnData {
|
||||
@@ -133,6 +113,10 @@ pub(super) enum ViewOffset {
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ViewGesture {
|
||||
current_view_offset: f64,
|
||||
/// Animation for the extra offset to the current position.
|
||||
///
|
||||
/// For example, when we need to activate a specific window during a DnD scroll.
|
||||
animation: Option<Animation>,
|
||||
tracker: SwipeTracker,
|
||||
delta_from_tracker: f64,
|
||||
// The view offset we'll use if needed for activate_prev_column_on_removal.
|
||||
@@ -283,8 +267,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
activate_prev_column_on_removal: None,
|
||||
view_offset_before_fullscreen: None,
|
||||
closing_windows: Vec::new(),
|
||||
insert_hint: None,
|
||||
insert_hint_element: InsertHintElement::new(options.insert_hint),
|
||||
view_size,
|
||||
working_area,
|
||||
scale,
|
||||
@@ -307,20 +289,21 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
data.update(column);
|
||||
}
|
||||
|
||||
self.insert_hint_element.update_config(options.insert_hint);
|
||||
|
||||
self.view_size = view_size;
|
||||
self.working_area = working_area;
|
||||
self.scale = scale;
|
||||
self.options = options;
|
||||
|
||||
// Apply always-center and such right away.
|
||||
if !self.columns.is_empty() && !self.view_offset.is_gesture() {
|
||||
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for tile in self.tiles_mut() {
|
||||
tile.update_shaders();
|
||||
}
|
||||
|
||||
self.insert_hint_element.update_shaders();
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
@@ -347,6 +330,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
gesture.dnd_nonzero_start_time = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(anim) = &mut gesture.animation {
|
||||
if anim.is_done() {
|
||||
gesture.animation = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for col in &mut self.columns {
|
||||
@@ -360,7 +349,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.view_offset.is_animation()
|
||||
self.view_offset.is_animation_ongoing()
|
||||
|| self.columns.iter().any(Column::are_animations_ongoing)
|
||||
|| !self.closing_windows.is_empty()
|
||||
}
|
||||
@@ -382,18 +371,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
let view_rect = Rectangle::new(col_pos, view_size);
|
||||
col.update_render_elements(is_active, view_rect);
|
||||
}
|
||||
|
||||
if let Some(insert_hint) = &self.insert_hint {
|
||||
if let Some(area) = self.insert_hint_area(insert_hint) {
|
||||
let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size);
|
||||
self.insert_hint_element.update_render_elements(
|
||||
area.size,
|
||||
view_rect,
|
||||
insert_hint.corner_radius,
|
||||
self.scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tiles(&self) -> impl Iterator<Item = &Tile<W>> + '_ {
|
||||
@@ -616,6 +593,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
return self.compute_new_view_offset_for_column_fit(target_x, idx);
|
||||
};
|
||||
|
||||
// Activating the same column.
|
||||
if prev_idx == idx {
|
||||
return self.compute_new_view_offset_for_column_fit(target_x, idx);
|
||||
}
|
||||
|
||||
// Always take the left or right neighbor of the target as the source.
|
||||
let source_idx = if prev_idx > idx {
|
||||
min(idx + 1, self.columns.len() - 1)
|
||||
@@ -664,8 +646,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
new_view_offset: f64,
|
||||
config: niri_config::Animation,
|
||||
) {
|
||||
self.view_offset.cancel_gesture();
|
||||
|
||||
let new_col_x = self.column_x(idx);
|
||||
let old_col_x = self.column_x(self.active_column_idx);
|
||||
let offset_delta = old_col_x - new_col_x;
|
||||
@@ -682,14 +662,28 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: also compute and use current velocity.
|
||||
self.view_offset = ViewOffset::Animation(Animation::new(
|
||||
self.clock.clone(),
|
||||
self.view_offset.current(),
|
||||
new_view_offset,
|
||||
0.,
|
||||
config,
|
||||
));
|
||||
match &mut self.view_offset {
|
||||
ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some() => {
|
||||
gesture.stationary_view_offset = new_view_offset;
|
||||
|
||||
let current_pos = gesture.current_view_offset - gesture.delta_from_tracker;
|
||||
gesture.delta_from_tracker = new_view_offset - current_pos;
|
||||
let offset_delta = new_view_offset - gesture.current_view_offset;
|
||||
gesture.current_view_offset = new_view_offset;
|
||||
|
||||
gesture.animate_from(-offset_delta, self.clock.clone(), config);
|
||||
}
|
||||
_ => {
|
||||
// FIXME: also compute and use current velocity.
|
||||
self.view_offset = ViewOffset::Animation(Animation::new(
|
||||
self.clock.clone(),
|
||||
self.view_offset.current(),
|
||||
new_view_offset,
|
||||
0.,
|
||||
config,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn animate_view_offset_to_column_centered(
|
||||
@@ -735,7 +729,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
}
|
||||
|
||||
fn activate_column_with_anim_config(&mut self, idx: usize, config: niri_config::Animation) {
|
||||
if self.active_column_idx == idx {
|
||||
if self.active_column_idx == idx
|
||||
// During a DnD scroll, animate even when activating the same window, for DnD hold.
|
||||
&& (self.columns.is_empty() || !self.view_offset.is_dnd_scroll())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -746,26 +743,17 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
config,
|
||||
);
|
||||
|
||||
self.active_column_idx = idx;
|
||||
if self.active_column_idx != idx {
|
||||
self.active_column_idx = idx;
|
||||
|
||||
// A different column was activated; reset the flag.
|
||||
self.activate_prev_column_on_removal = None;
|
||||
self.view_offset_before_fullscreen = None;
|
||||
self.interactive_resize = None;
|
||||
}
|
||||
|
||||
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
|
||||
if self.options.insert_hint.off {
|
||||
return;
|
||||
// A different column was activated; reset the flag.
|
||||
self.activate_prev_column_on_removal = None;
|
||||
self.view_offset_before_fullscreen = None;
|
||||
self.interactive_resize = None;
|
||||
}
|
||||
self.insert_hint = Some(insert_hint);
|
||||
}
|
||||
|
||||
pub fn clear_insert_hint(&mut self) {
|
||||
self.insert_hint = None;
|
||||
}
|
||||
|
||||
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
|
||||
pub(super) fn insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
|
||||
if self.columns.is_empty() {
|
||||
return InsertPosition::NewColumn(0);
|
||||
}
|
||||
@@ -1612,7 +1600,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
// Preserve the camera position when moving to the left.
|
||||
let view_offset_delta = -self.column_x(self.active_column_idx) + current_col_x;
|
||||
self.view_offset.cancel_gesture();
|
||||
self.view_offset.offset(view_offset_delta);
|
||||
|
||||
// The column we just moved is offset by the difference between its new and old position.
|
||||
@@ -2152,6 +2139,64 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.center_column();
|
||||
}
|
||||
|
||||
pub fn center_visible_columns(&mut self) {
|
||||
if self.columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_centering_focused_column() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consider the end of an ongoing animation because that's what compute to fit does too.
|
||||
let view_x = self.target_view_pos();
|
||||
let working_x = self.working_area.loc.x;
|
||||
let working_w = self.working_area.size.w;
|
||||
|
||||
// Count all columns that are fully visible inside the working area.
|
||||
let mut width_taken = 0.;
|
||||
let mut leftmost_col_x = None;
|
||||
let mut active_col_x = None;
|
||||
|
||||
let gap = self.options.gaps;
|
||||
let col_xs = self.column_xs(self.data.iter().copied());
|
||||
for (idx, col_x) in col_xs.take(self.columns.len()).enumerate() {
|
||||
if col_x < view_x + working_x + gap {
|
||||
// Column goes off-screen to the left.
|
||||
continue;
|
||||
}
|
||||
|
||||
leftmost_col_x.get_or_insert(col_x);
|
||||
|
||||
let width = self.data[idx].width;
|
||||
if view_x + working_x + working_w < col_x + width + gap {
|
||||
// Column goes off-screen to the right. We can stop here.
|
||||
break;
|
||||
}
|
||||
|
||||
if idx == self.active_column_idx {
|
||||
active_col_x = Some(col_x);
|
||||
}
|
||||
|
||||
width_taken += width + gap;
|
||||
}
|
||||
|
||||
if active_col_x.is_none() {
|
||||
// The active column wasn't fully on screen, so we can't meaningfully do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
let col = &mut self.columns[self.active_column_idx];
|
||||
cancel_resize_for_column(&mut self.interactive_resize, col);
|
||||
|
||||
let free_space = working_w - width_taken + gap;
|
||||
let new_view_x = leftmost_col_x.unwrap() - free_space / 2. - working_x;
|
||||
|
||||
self.animate_view_offset(self.active_column_idx, new_view_x - active_col_x.unwrap());
|
||||
// Just in case.
|
||||
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
||||
}
|
||||
|
||||
pub fn view_pos(&self) -> f64 {
|
||||
self.column_x(self.active_column_idx) + self.view_offset.current()
|
||||
}
|
||||
@@ -2274,8 +2319,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
|
||||
let mut hint_area = match insert_hint.position {
|
||||
pub(super) fn insert_hint_area(
|
||||
&self,
|
||||
position: InsertPosition,
|
||||
) -> Option<Rectangle<f64, Logical>> {
|
||||
let mut hint_area = match position {
|
||||
InsertPosition::NewColumn(column_index) => {
|
||||
if column_index == 0 || column_index == self.columns.len() {
|
||||
let size =
|
||||
@@ -2366,19 +2414,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
hint_area.loc.x -= self.view_pos();
|
||||
}
|
||||
|
||||
let view_size = self.view_size;
|
||||
|
||||
// Make sure the hint is at least partially visible.
|
||||
if matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
|
||||
hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
|
||||
hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
|
||||
}
|
||||
|
||||
// Round to physical pixels.
|
||||
hint_area = hint_area
|
||||
.to_physical_precise_round(self.scale)
|
||||
.to_logical(self.scale);
|
||||
|
||||
Some(hint_area)
|
||||
}
|
||||
|
||||
@@ -2729,17 +2764,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
// Draw the insert hint.
|
||||
if let Some(insert_hint) = &self.insert_hint {
|
||||
if let Some(area) = self.insert_hint_area(insert_hint) {
|
||||
rv.extend(
|
||||
self.insert_hint_element
|
||||
.render(renderer, area.loc)
|
||||
.map(ScrollingSpaceRenderElement::InsertHint),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the closing windows on top of the other windows.
|
||||
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
|
||||
for closing in self.closing_windows.iter().rev() {
|
||||
@@ -2854,6 +2878,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
let gesture = ViewGesture {
|
||||
current_view_offset: self.view_offset.current(),
|
||||
animation: None,
|
||||
tracker: SwipeTracker::new(),
|
||||
delta_from_tracker: self.view_offset.current(),
|
||||
stationary_view_offset: self.view_offset.stationary(),
|
||||
@@ -2876,6 +2901,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
let gesture = ViewGesture {
|
||||
current_view_offset: self.view_offset.current(),
|
||||
animation: None,
|
||||
tracker: SwipeTracker::new(),
|
||||
delta_from_tracker: self.view_offset.current(),
|
||||
stationary_view_offset: self.view_offset.stationary(),
|
||||
@@ -2916,14 +2942,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) {
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) -> bool {
|
||||
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(last_time) = gesture.dnd_last_event_time else {
|
||||
// Not a DnD scroll.
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let config = &self.options.gestures.dnd_edge_view_scroll;
|
||||
@@ -2934,7 +2960,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
if delta == 0. {
|
||||
// We're outside the scrolling zone.
|
||||
gesture.dnd_nonzero_start_time = None;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
|
||||
@@ -2943,7 +2969,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
// monitors.
|
||||
let delay = Duration::from_millis(u64::from(config.delay_ms));
|
||||
if now.saturating_sub(nonzero_start) < delay {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
let time_delta = now.saturating_sub(last_time).as_secs_f64();
|
||||
@@ -2987,9 +3013,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
gesture.delta_from_tracker += clamped_offset - view_offset;
|
||||
gesture.current_view_offset = clamped_offset;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {
|
||||
pub fn view_offset_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
|
||||
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
||||
return false;
|
||||
};
|
||||
@@ -3279,7 +3306,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
self.view_offset_gesture_end(false, None);
|
||||
self.view_offset_gesture_end(None);
|
||||
}
|
||||
|
||||
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
||||
@@ -3570,7 +3597,10 @@ impl ViewOffset {
|
||||
match self {
|
||||
ViewOffset::Static(offset) => *offset,
|
||||
ViewOffset::Animation(anim) => anim.value(),
|
||||
ViewOffset::Gesture(gesture) => gesture.current_view_offset,
|
||||
ViewOffset::Gesture(gesture) => {
|
||||
gesture.current_view_offset
|
||||
+ gesture.animation.as_ref().map_or(0., |anim| anim.value())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3600,21 +3630,30 @@ impl ViewOffset {
|
||||
matches!(self, Self::Static(_))
|
||||
}
|
||||
|
||||
pub fn is_animation(&self) -> bool {
|
||||
matches!(self, Self::Animation(_))
|
||||
}
|
||||
|
||||
pub fn is_gesture(&self) -> bool {
|
||||
matches!(self, Self::Gesture(_))
|
||||
}
|
||||
|
||||
pub fn is_dnd_scroll(&self) -> bool {
|
||||
matches!(&self, ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some())
|
||||
}
|
||||
|
||||
pub fn is_animation_ongoing(&self) -> bool {
|
||||
match self {
|
||||
ViewOffset::Static(_) => false,
|
||||
ViewOffset::Animation(_) => true,
|
||||
ViewOffset::Gesture(gesture) => gesture.animation.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset(&mut self, delta: f64) {
|
||||
match self {
|
||||
ViewOffset::Static(offset) => *offset += delta,
|
||||
ViewOffset::Animation(anim) => anim.offset(delta),
|
||||
ViewOffset::Gesture(_gesture) => {
|
||||
// Is this needed?
|
||||
error!("cancel gesture before offsetting");
|
||||
ViewOffset::Gesture(gesture) => {
|
||||
gesture.stationary_view_offset += delta;
|
||||
gesture.delta_from_tracker += delta;
|
||||
gesture.current_view_offset += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3630,6 +3669,13 @@ impl ViewOffset {
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewGesture {
|
||||
fn animate_from(&mut self, from: f64, clock: Clock, config: niri_config::Animation) {
|
||||
let current = self.animation.as_ref().map_or(0., Animation::value);
|
||||
self.animation = Some(Animation::new(clock, from + current, 0., 0., config));
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnData {
|
||||
pub fn new<W: LayoutElement>(column: &Column<W>) -> Self {
|
||||
let mut rv = Self { width: 0. };
|
||||
@@ -3832,8 +3878,9 @@ impl<W: LayoutElement> Column<W> {
|
||||
.enumerate()
|
||||
.map(|(tile_idx, (tile, tile_off))| {
|
||||
let is_active = tile_idx == active_idx;
|
||||
let is_urgent = tile.window().is_urgent();
|
||||
let tile_pos = tile_off + tile.render_offset();
|
||||
TabInfo::from_tile(tile, tile_pos, is_active, &config)
|
||||
TabInfo::from_tile(tile, tile_pos, is_active, is_urgent, &config)
|
||||
});
|
||||
|
||||
// Hide the tab indicator in fullscreen. If you have it configured to overlap the window,
|
||||
|
||||
@@ -10,7 +10,9 @@ use crate::animation::{Animation, Clock};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::{floor_logical_in_physical_max1, round_logical_in_physical};
|
||||
use crate::utils::{
|
||||
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TabIndicator {
|
||||
@@ -77,12 +79,14 @@ impl TabIndicator {
|
||||
scale: f64,
|
||||
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
||||
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
|
||||
|
||||
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
|
||||
|
||||
let width = round(self.config.width.0);
|
||||
let gap = round(self.config.gap.0);
|
||||
let gaps_between = round(self.config.gaps_between_tabs.0);
|
||||
let width = round_max1(self.config.width.0);
|
||||
let gap = self.config.gap.0;
|
||||
let gap = round_max1(gap.abs()).copysign(gap);
|
||||
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
|
||||
|
||||
let position = self.config.position;
|
||||
let side = match position {
|
||||
@@ -346,13 +350,16 @@ impl TabInfo {
|
||||
tile: &Tile<W>,
|
||||
position: Point<f64, Logical>,
|
||||
is_active: bool,
|
||||
is_urgent: bool,
|
||||
config: &niri_config::TabIndicator,
|
||||
) -> Self {
|
||||
let rules = tile.window().rules();
|
||||
let rule = rules.tab_indicator;
|
||||
|
||||
let gradient_from_rule = || {
|
||||
let (color, gradient) = if is_active {
|
||||
let (color, gradient) = if is_urgent {
|
||||
(rule.urgent_color, rule.urgent_gradient)
|
||||
} else if is_active {
|
||||
(rule.active_color, rule.active_gradient)
|
||||
} else {
|
||||
(rule.inactive_color, rule.inactive_gradient)
|
||||
@@ -362,7 +369,9 @@ impl TabInfo {
|
||||
};
|
||||
|
||||
let gradient_from_config = || {
|
||||
let (color, gradient) = if is_active {
|
||||
let (color, gradient) = if is_urgent {
|
||||
(config.urgent_color, config.urgent_gradient)
|
||||
} else if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
@@ -382,7 +391,9 @@ impl TabInfo {
|
||||
focus_ring_config
|
||||
};
|
||||
|
||||
let (color, gradient) = if is_active {
|
||||
let (color, gradient) = if is_urgent {
|
||||
(config.urgent_color, config.urgent_gradient)
|
||||
} else if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
|
||||
+165
-29
@@ -261,6 +261,10 @@ impl LayoutElement for TestWindow {
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_urgent(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
|
||||
@@ -460,6 +464,7 @@ enum Op {
|
||||
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
|
||||
id: Option<usize>,
|
||||
},
|
||||
CenterVisibleColumns,
|
||||
FocusWorkspaceDown,
|
||||
FocusWorkspaceUp,
|
||||
FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
|
||||
@@ -473,9 +478,9 @@ enum Op {
|
||||
#[proptest(strategy = "0..=4usize")]
|
||||
workspace_idx: usize,
|
||||
},
|
||||
MoveColumnToWorkspaceDown,
|
||||
MoveColumnToWorkspaceUp,
|
||||
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
|
||||
MoveColumnToWorkspaceDown(bool),
|
||||
MoveColumnToWorkspaceUp(bool),
|
||||
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize, bool),
|
||||
MoveWorkspaceDown,
|
||||
MoveWorkspaceUp,
|
||||
MoveWorkspaceToIndex {
|
||||
@@ -508,7 +513,13 @@ enum Op {
|
||||
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
|
||||
target_ws_idx: Option<usize>,
|
||||
},
|
||||
MoveColumnToOutput(#[proptest(strategy = "1..=5usize")] usize),
|
||||
MoveColumnToOutput {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
output_id: usize,
|
||||
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
|
||||
target_ws_idx: Option<usize>,
|
||||
activate: bool,
|
||||
},
|
||||
SwitchPresetColumnWidth,
|
||||
SwitchPresetWindowWidth {
|
||||
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
|
||||
@@ -576,6 +587,8 @@ enum Op {
|
||||
ViewOffsetGestureBegin {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
output_idx: usize,
|
||||
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
|
||||
workspace_idx: Option<usize>,
|
||||
is_touchpad: bool,
|
||||
},
|
||||
ViewOffsetGestureUpdate {
|
||||
@@ -599,9 +612,15 @@ enum Op {
|
||||
is_touchpad: bool,
|
||||
},
|
||||
WorkspaceSwitchGestureEnd {
|
||||
cancelled: bool,
|
||||
is_touchpad: Option<bool>,
|
||||
},
|
||||
OverviewGestureBegin,
|
||||
OverviewGestureUpdate {
|
||||
#[proptest(strategy = "-400f64..400f64")]
|
||||
delta: f64,
|
||||
timestamp: Duration,
|
||||
},
|
||||
OverviewGestureEnd,
|
||||
InteractiveMoveBegin {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
window: usize,
|
||||
@@ -657,6 +676,7 @@ enum Op {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
window: usize,
|
||||
},
|
||||
ToggleOverview,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -1049,6 +1069,7 @@ impl Op {
|
||||
let id = id.filter(|id| layout.has_window(id));
|
||||
layout.center_window(id.as_ref());
|
||||
}
|
||||
Op::CenterVisibleColumns => layout.center_visible_columns(),
|
||||
Op::FocusWorkspaceDown => layout.switch_workspace_down(),
|
||||
Op::FocusWorkspaceUp => layout.switch_workspace_up(),
|
||||
Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
|
||||
@@ -1065,9 +1086,9 @@ impl Op {
|
||||
let window_id = window_id.filter(|id| layout.has_window(id));
|
||||
layout.move_to_workspace(window_id.as_ref(), workspace_idx, ActivateWindow::Smart);
|
||||
}
|
||||
Op::MoveColumnToWorkspaceDown => layout.move_column_to_workspace_down(),
|
||||
Op::MoveColumnToWorkspaceUp => layout.move_column_to_workspace_up(),
|
||||
Op::MoveColumnToWorkspace(idx) => layout.move_column_to_workspace(idx),
|
||||
Op::MoveColumnToWorkspaceDown(focus) => layout.move_column_to_workspace_down(focus),
|
||||
Op::MoveColumnToWorkspaceUp(focus) => layout.move_column_to_workspace_up(focus),
|
||||
Op::MoveColumnToWorkspace(idx, focus) => layout.move_column_to_workspace(idx, focus),
|
||||
Op::MoveWindowToOutput {
|
||||
window_id,
|
||||
output_id: id,
|
||||
@@ -1088,13 +1109,17 @@ impl Op {
|
||||
ActivateWindow::Smart,
|
||||
);
|
||||
}
|
||||
Op::MoveColumnToOutput(id) => {
|
||||
Op::MoveColumnToOutput {
|
||||
output_id: id,
|
||||
target_ws_idx,
|
||||
activate,
|
||||
} => {
|
||||
let name = format!("output{id}");
|
||||
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
layout.move_column_to_output(&output);
|
||||
layout.move_column_to_output(&output, target_ws_idx, activate);
|
||||
}
|
||||
Op::MoveWorkspaceDown => layout.move_workspace_down(),
|
||||
Op::MoveWorkspaceUp => layout.move_workspace_up(),
|
||||
@@ -1345,6 +1370,7 @@ impl Op {
|
||||
}
|
||||
Op::ViewOffsetGestureBegin {
|
||||
output_idx: id,
|
||||
workspace_idx,
|
||||
is_touchpad: normalize,
|
||||
} => {
|
||||
let name = format!("output{id}");
|
||||
@@ -1352,7 +1378,7 @@ impl Op {
|
||||
return;
|
||||
};
|
||||
|
||||
layout.view_offset_gesture_begin(&output, normalize);
|
||||
layout.view_offset_gesture_begin(&output, workspace_idx, normalize);
|
||||
}
|
||||
Op::ViewOffsetGestureUpdate {
|
||||
delta,
|
||||
@@ -1362,8 +1388,7 @@ impl Op {
|
||||
layout.view_offset_gesture_update(delta, timestamp, is_touchpad);
|
||||
}
|
||||
Op::ViewOffsetGestureEnd { is_touchpad } => {
|
||||
// We don't handle cancels in this gesture.
|
||||
layout.view_offset_gesture_end(false, is_touchpad);
|
||||
layout.view_offset_gesture_end(is_touchpad);
|
||||
}
|
||||
Op::WorkspaceSwitchGestureBegin {
|
||||
output_idx: id,
|
||||
@@ -1383,11 +1408,17 @@ impl Op {
|
||||
} => {
|
||||
layout.workspace_switch_gesture_update(delta, timestamp, is_touchpad);
|
||||
}
|
||||
Op::WorkspaceSwitchGestureEnd {
|
||||
cancelled,
|
||||
is_touchpad,
|
||||
} => {
|
||||
layout.workspace_switch_gesture_end(cancelled, is_touchpad);
|
||||
Op::WorkspaceSwitchGestureEnd { is_touchpad } => {
|
||||
layout.workspace_switch_gesture_end(is_touchpad);
|
||||
}
|
||||
Op::OverviewGestureBegin => {
|
||||
layout.overview_gesture_begin();
|
||||
}
|
||||
Op::OverviewGestureUpdate { delta, timestamp } => {
|
||||
layout.overview_gesture_update(delta, timestamp);
|
||||
}
|
||||
Op::OverviewGestureEnd => {
|
||||
layout.overview_gesture_end();
|
||||
}
|
||||
Op::InteractiveMoveBegin {
|
||||
window,
|
||||
@@ -1442,6 +1473,9 @@ impl Op {
|
||||
Op::InteractiveResizeEnd { window } => {
|
||||
layout.interactive_resize_end(&window);
|
||||
}
|
||||
Op::ToggleOverview => {
|
||||
layout.toggle_overview();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1542,10 +1576,10 @@ fn operations_dont_panic() {
|
||||
window_id: None,
|
||||
workspace_idx: 2,
|
||||
},
|
||||
Op::MoveColumnToWorkspaceDown,
|
||||
Op::MoveColumnToWorkspaceUp,
|
||||
Op::MoveColumnToWorkspace(1),
|
||||
Op::MoveColumnToWorkspace(2),
|
||||
Op::MoveColumnToWorkspaceDown(true),
|
||||
Op::MoveColumnToWorkspaceUp(true),
|
||||
Op::MoveColumnToWorkspace(1, true),
|
||||
Op::MoveColumnToWorkspace(2, true),
|
||||
Op::MoveWindowDown,
|
||||
Op::MoveWindowDownOrToWorkspaceDown,
|
||||
Op::MoveWindowUp,
|
||||
@@ -1717,11 +1751,11 @@ fn operations_from_starting_state_dont_panic() {
|
||||
window_id: None,
|
||||
workspace_idx: 3,
|
||||
},
|
||||
Op::MoveColumnToWorkspaceDown,
|
||||
Op::MoveColumnToWorkspaceUp,
|
||||
Op::MoveColumnToWorkspace(1),
|
||||
Op::MoveColumnToWorkspace(2),
|
||||
Op::MoveColumnToWorkspace(3),
|
||||
Op::MoveColumnToWorkspaceDown(true),
|
||||
Op::MoveColumnToWorkspaceUp(true),
|
||||
Op::MoveColumnToWorkspace(1, true),
|
||||
Op::MoveColumnToWorkspace(2, true),
|
||||
Op::MoveColumnToWorkspace(3, true),
|
||||
Op::MoveWindowDown,
|
||||
Op::MoveWindowDownOrToWorkspaceDown,
|
||||
Op::MoveWindowUp,
|
||||
@@ -2040,8 +2074,8 @@ fn workspace_transfer_during_switch_gets_cleaned_up() {
|
||||
},
|
||||
Op::RemoveOutput(1),
|
||||
Op::AddOutput(2),
|
||||
Op::MoveColumnToWorkspaceDown,
|
||||
Op::MoveColumnToWorkspaceDown,
|
||||
Op::MoveColumnToWorkspaceDown(true),
|
||||
Op::MoveColumnToWorkspaceDown(true),
|
||||
Op::AddOutput(1),
|
||||
];
|
||||
|
||||
@@ -2265,6 +2299,7 @@ fn unfullscreen_view_offset_not_reset_on_gesture() {
|
||||
Op::FullscreenWindow(1),
|
||||
Op::ViewOffsetGestureBegin {
|
||||
output_idx: 1,
|
||||
workspace_idx: None,
|
||||
is_touchpad: true,
|
||||
},
|
||||
Op::ViewOffsetGestureEnd {
|
||||
@@ -3335,6 +3370,107 @@ fn interactive_resize_on_pending_unfullscreen_column() {
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_column_to_workspace_unfocused_with_multiple_monitors() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::SetWorkspaceName {
|
||||
new_ws_name: 101,
|
||||
ws_name: None,
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::FocusWorkspaceDown,
|
||||
Op::SetWorkspaceName {
|
||||
new_ws_name: 102,
|
||||
ws_name: None,
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(2),
|
||||
},
|
||||
Op::AddOutput(2),
|
||||
Op::FocusOutput(2),
|
||||
Op::SetWorkspaceName {
|
||||
new_ws_name: 201,
|
||||
ws_name: None,
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(3),
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(4),
|
||||
},
|
||||
Op::MoveColumnToOutput {
|
||||
output_id: 1,
|
||||
target_ws_idx: Some(0),
|
||||
activate: false,
|
||||
},
|
||||
Op::FocusOutput(1),
|
||||
];
|
||||
|
||||
let layout = check_ops(&ops);
|
||||
|
||||
assert_eq!(layout.active_workspace().unwrap().name().unwrap(), "ws102");
|
||||
|
||||
for (mon, win) in layout.windows() {
|
||||
let mon = mon.unwrap();
|
||||
let ws = mon
|
||||
.workspaces
|
||||
.iter()
|
||||
.find(|w| w.has_window(win.id()))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ws.name().unwrap(),
|
||||
match win.id() {
|
||||
1 | 4 => "ws101",
|
||||
2 => "ws102",
|
||||
3 => "ws201",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_move_unfullscreen_to_floating_stops_dnd_scroll() {
|
||||
let ops = [
|
||||
Op::AddOutput(3),
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams {
|
||||
is_floating: true,
|
||||
..TestWindowParams::new(4)
|
||||
},
|
||||
},
|
||||
// This moves the window to tiling.
|
||||
Op::SetFullscreenWindow {
|
||||
window: 4,
|
||||
is_fullscreen: true,
|
||||
},
|
||||
// This starts a DnD scroll since we're dragging a tiled window.
|
||||
Op::InteractiveMoveBegin {
|
||||
window: 4,
|
||||
output_idx: 3,
|
||||
px: 0.0,
|
||||
py: 0.0,
|
||||
},
|
||||
// This will cause the window to unfullscreen to floating, and should stop the DnD scroll
|
||||
// since we're no longer dragging a tiled window, but rather a floating one.
|
||||
Op::InteractiveMoveUpdate {
|
||||
window: 4,
|
||||
dx: 0.0,
|
||||
dy: 15035.31210741684,
|
||||
output_idx: 3,
|
||||
px: 0.0,
|
||||
py: 0.0,
|
||||
},
|
||||
Op::InteractiveMoveEnd { window: 4 },
|
||||
];
|
||||
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
|
||||
if parent_id == id {
|
||||
return true;
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::round_logical_in_physical;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||
|
||||
/// Toplevel window with decorations.
|
||||
#[derive(Debug)]
|
||||
@@ -366,6 +366,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.animated_window_size(),
|
||||
is_active,
|
||||
!draw_border_with_background,
|
||||
self.window.is_urgent(),
|
||||
Rectangle::new(
|
||||
view_rect.loc - Point::from((border_width, border_width)),
|
||||
view_rect.size,
|
||||
@@ -400,6 +401,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.animated_tile_size(),
|
||||
is_active,
|
||||
!draw_focus_ring_with_background,
|
||||
self.window.is_urgent(),
|
||||
view_rect,
|
||||
radius,
|
||||
self.scale,
|
||||
@@ -798,9 +800,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
return Point::from((0., 0.));
|
||||
}
|
||||
|
||||
let now = self.clock.now().as_secs_f64();
|
||||
let amplitude = self.view_size.h / 96.;
|
||||
let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.);
|
||||
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
|
||||
let y = round_logical_in_physical(self.scale, y);
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
+94
-20
@@ -2,7 +2,9 @@ use std::cmp::max;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
|
||||
use niri_config::{
|
||||
CenterFocusedColumn, CornerRadius, OutputName, PresetSize, Workspace as WorkspaceConfig,
|
||||
};
|
||||
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::{layer_map_for_output, Window};
|
||||
@@ -15,16 +17,18 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
|
||||
use super::scrolling::{
|
||||
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
|
||||
ScrollingSpaceRenderElement,
|
||||
Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement,
|
||||
};
|
||||
use super::shadow::Shadow;
|
||||
use super::tile::{Tile, TileRenderSnapshot};
|
||||
use super::{
|
||||
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
|
||||
ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options,
|
||||
RemovedTile, SizeFrac,
|
||||
};
|
||||
use crate::animation::Clock;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
@@ -80,6 +84,9 @@ pub struct Workspace<W: LayoutElement> {
|
||||
/// zones.
|
||||
working_area: Rectangle<f64, Logical>,
|
||||
|
||||
/// This workspace's shadow in the overview.
|
||||
shadow: Shadow,
|
||||
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
|
||||
@@ -228,6 +235,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config =
|
||||
compute_workspace_shadow_config(options.overview.workspace_shadow, view_size);
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -237,6 +247,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
transform: output.current_transform(),
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
output: Some(output),
|
||||
clock,
|
||||
base_options,
|
||||
@@ -281,6 +292,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config =
|
||||
compute_workspace_shadow_config(options.overview.workspace_shadow, view_size);
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -291,6 +305,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
original_output,
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
clock,
|
||||
base_options,
|
||||
options,
|
||||
@@ -343,6 +358,14 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
self.floating
|
||||
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
|
||||
|
||||
self.shadow.update_render_elements(
|
||||
self.view_size,
|
||||
true,
|
||||
CornerRadius::default(),
|
||||
self.scale.fractional_scale(),
|
||||
1.,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, base_options: Rc<Options>) {
|
||||
@@ -363,6 +386,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config =
|
||||
compute_workspace_shadow_config(options.overview.workspace_shadow, self.view_size);
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
self.base_options = base_options;
|
||||
self.options = options;
|
||||
}
|
||||
@@ -370,6 +397,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.scrolling.update_shaders();
|
||||
self.floating.update_shaders();
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
|
||||
@@ -501,6 +529,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
scale.fractional_scale(),
|
||||
self.options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config =
|
||||
compute_workspace_shadow_config(self.options.overview.workspace_shadow, size);
|
||||
self.shadow.update_config(shadow_config);
|
||||
}
|
||||
|
||||
if scale_transform_changed {
|
||||
@@ -1068,6 +1100,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_visible_columns(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.center_visible_columns();
|
||||
}
|
||||
|
||||
pub fn toggle_width(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.toggle_window_width(None);
|
||||
@@ -1409,7 +1448,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
|
||||
) -> (
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
) {
|
||||
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
|
||||
let scrolling = self
|
||||
.scrolling
|
||||
@@ -1424,8 +1466,16 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
.render_elements(renderer, view_rect, target, floating_focus_ring);
|
||||
floating.into_iter().map(WorkspaceRenderElement::from)
|
||||
});
|
||||
let floating = floating.into_iter().flatten();
|
||||
|
||||
floating.into_iter().flatten().chain(scrolling)
|
||||
(floating, scrolling)
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)))
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
@@ -1565,6 +1615,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.scrolling.scroll_amount_to_activate(window)
|
||||
}
|
||||
|
||||
pub fn is_urgent(&self) -> bool {
|
||||
self.windows().any(|win| win.is_urgent())
|
||||
}
|
||||
|
||||
pub fn activate_window(&mut self, window: &W::Id) -> bool {
|
||||
if self.floating.activate_window(window) {
|
||||
self.floating_is_active = FloatingActive::Yes;
|
||||
@@ -1593,16 +1647,15 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
|
||||
self.scrolling.set_insert_hint(insert_hint);
|
||||
pub(super) fn scrolling_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
|
||||
self.scrolling.insert_position(pos)
|
||||
}
|
||||
|
||||
pub fn clear_insert_hint(&mut self) {
|
||||
self.scrolling.clear_insert_hint();
|
||||
}
|
||||
|
||||
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
|
||||
self.scrolling.get_insert_position(pos)
|
||||
pub(super) fn insert_hint_area(
|
||||
&self,
|
||||
position: InsertPosition,
|
||||
) -> Option<Rectangle<f64, Logical>> {
|
||||
self.scrolling.insert_hint_area(position)
|
||||
}
|
||||
|
||||
pub fn view_offset_gesture_begin(&mut self, is_touchpad: bool) {
|
||||
@@ -1619,16 +1672,15 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
.view_offset_gesture_update(delta_x, timestamp, is_touchpad)
|
||||
}
|
||||
|
||||
pub fn view_offset_gesture_end(&mut self, cancelled: bool, is_touchpad: Option<bool>) -> bool {
|
||||
self.scrolling
|
||||
.view_offset_gesture_end(cancelled, is_touchpad)
|
||||
pub fn view_offset_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
|
||||
self.scrolling.view_offset_gesture_end(is_touchpad)
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_begin(&mut self) {
|
||||
self.scrolling.dnd_scroll_gesture_begin();
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) {
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
|
||||
let config = &self.options.gestures.dnd_edge_view_scroll;
|
||||
let trigger_width = config.trigger_width.0;
|
||||
|
||||
@@ -1654,8 +1706,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
// Normalize to [0, 1].
|
||||
delta / trigger_width
|
||||
};
|
||||
let delta = delta * speed;
|
||||
|
||||
self.scrolling.dnd_scroll_gesture_scroll(delta);
|
||||
self.scrolling.dnd_scroll_gesture_scroll(delta)
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_end(&mut self) {
|
||||
@@ -1706,6 +1759,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.floating.logical_to_size_frac(logical_pos)
|
||||
}
|
||||
|
||||
pub fn working_area(&self) -> Rectangle<f64, Logical> {
|
||||
self.working_area
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn scrolling(&self) -> &ScrollingSpace<W> {
|
||||
&self.scrolling
|
||||
@@ -1775,6 +1832,23 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_working_area(output: &Output) -> Rectangle<f64, Logical> {
|
||||
pub(super) fn compute_working_area(output: &Output) -> Rectangle<f64, Logical> {
|
||||
layer_map_for_output(output).non_exclusive_zone().to_f64()
|
||||
}
|
||||
|
||||
fn compute_workspace_shadow_config(
|
||||
config: niri_config::WorkspaceShadow,
|
||||
view_size: Size<f64, Logical>,
|
||||
) -> niri_config::Shadow {
|
||||
// Gaps between workspaces are a multiple of the view height, so shadow settings should also be
|
||||
// normalized to the view height to prevent them from overlapping on lower resolutions.
|
||||
let norm = view_size.h / 1080.;
|
||||
|
||||
let mut config = niri_config::Shadow::from(config);
|
||||
config.softness.0 *= norm;
|
||||
config.spread.0 *= norm;
|
||||
config.offset.x.0 *= norm;
|
||||
config.offset.y.0 *= norm;
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
+5
-7
@@ -161,12 +161,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut config_errored = false;
|
||||
let mut config = Config::load(&path)
|
||||
.map_err(|err| {
|
||||
warn!("{err:?}");
|
||||
config_errored = true;
|
||||
})
|
||||
let config_load_result = Config::load(&path);
|
||||
let config_errored = config_load_result.is_err();
|
||||
let mut config = config_load_result
|
||||
.map_err(|err| warn!("{err:?}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
@@ -355,7 +353,7 @@ fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
|
||||
let system_path = system_config_path();
|
||||
if let Some(path) = default_config_path() {
|
||||
if path.exists() {
|
||||
return (path.clone(), path, true);
|
||||
return (path.clone(), path, false);
|
||||
}
|
||||
|
||||
if system_path.exists() {
|
||||
|
||||
+530
-131
File diff suppressed because it is too large
Load Diff
@@ -83,6 +83,7 @@ pub struct Cast {
|
||||
scheduled_redraw: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum CastState {
|
||||
ResizePending {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::Color32F;
|
||||
use smithay::utils::Scale;
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
use super::solid_color::SolidColorRenderElement;
|
||||
use crate::niri::OutputRenderElements;
|
||||
|
||||
pub fn draw_opaque_regions<R: NiriRenderer>(
|
||||
@@ -35,9 +36,9 @@ pub fn draw_opaque_regions<R: NiriRenderer>(
|
||||
for rect in opaque {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect,
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
[0., 0., 0.2, 0.2],
|
||||
Color32F::from([0., 0., 0.2, 0.2]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
|
||||
@@ -47,9 +48,9 @@ pub fn draw_opaque_regions<R: NiriRenderer>(
|
||||
for rect in semitransparent {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
rect,
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
[0.3, 0., 0., 0.3],
|
||||
Color32F::from([0.3, 0., 0., 0.3]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
|
||||
@@ -64,6 +65,10 @@ pub fn draw_damage<R: NiriRenderer>(
|
||||
) {
|
||||
let _span = tracy_client::span!("draw_damage");
|
||||
|
||||
let Ok((_, scale, _)) = damage_tracker.mode().try_into() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok((Some(damage), _)) = damage_tracker.damage_output(1, elements) else {
|
||||
return;
|
||||
};
|
||||
@@ -71,9 +76,9 @@ pub fn draw_damage<R: NiriRenderer>(
|
||||
for rect in damage {
|
||||
let color = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
*rect,
|
||||
rect.to_f64().to_logical(scale),
|
||||
CommitCounter::default(),
|
||||
[0.3, 0., 0., 0.3],
|
||||
Color32F::from([0.3, 0., 0., 0.3]),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
elements.insert(0, OutputRenderElements::SolidColor(color));
|
||||
|
||||
@@ -13,7 +13,7 @@ use smithay::backend::renderer::utils::{
|
||||
CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions,
|
||||
};
|
||||
use smithay::backend::renderer::{
|
||||
Bind as _, Color32F, Frame as _, Offscreen as _, Renderer, Texture as _,
|
||||
Bind as _, Color32F, ContextId, Frame as _, Offscreen as _, Renderer, Texture as _,
|
||||
};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
@@ -36,8 +36,8 @@ pub struct OffscreenBuffer {
|
||||
struct Inner {
|
||||
/// The texture with offscreened contents.
|
||||
texture: GlesTexture,
|
||||
/// Id of the renderer that the texture comes from.
|
||||
renderer_id: usize,
|
||||
/// Id of the renderer context that the texture comes from.
|
||||
renderer_context_id: ContextId<GlesTexture>,
|
||||
/// Scale of the texture.
|
||||
scale: Scale<f64>,
|
||||
/// Damage tracker for drawing to the texture.
|
||||
@@ -50,7 +50,7 @@ struct Inner {
|
||||
pub struct OffscreenRenderElement {
|
||||
id: Id,
|
||||
texture: GlesTexture,
|
||||
renderer_id: usize,
|
||||
renderer_context_id: ContextId<GlesTexture>,
|
||||
scale: Scale<f64>,
|
||||
damage: DamageSnapshot<i32, Buffer>,
|
||||
offset: Point<f64, Logical>,
|
||||
@@ -92,7 +92,7 @@ impl OffscreenBuffer {
|
||||
let mut reason = "";
|
||||
if let Some(Inner {
|
||||
texture,
|
||||
renderer_id,
|
||||
renderer_context_id,
|
||||
..
|
||||
}) = inner.as_mut()
|
||||
{
|
||||
@@ -109,7 +109,7 @@ impl OffscreenBuffer {
|
||||
reason = "not unique";
|
||||
|
||||
*inner = None;
|
||||
} else if *renderer_id != renderer.id() {
|
||||
} else if *renderer_context_id != renderer.context_id() {
|
||||
reason = "renderer id changed";
|
||||
|
||||
*inner = None;
|
||||
@@ -134,7 +134,7 @@ impl OffscreenBuffer {
|
||||
|
||||
inner.insert(Inner {
|
||||
texture,
|
||||
renderer_id: renderer.id(),
|
||||
renderer_context_id: renderer.context_id(),
|
||||
scale,
|
||||
damage,
|
||||
outer_damage: DamageBag::default(),
|
||||
@@ -180,7 +180,7 @@ impl OffscreenBuffer {
|
||||
let elem = OffscreenRenderElement {
|
||||
id: self.id.clone(),
|
||||
texture: inner.texture.clone(),
|
||||
renderer_id: inner.renderer_id,
|
||||
renderer_context_id: inner.renderer_context_id.clone(),
|
||||
scale,
|
||||
damage: inner.outer_damage.snapshot(),
|
||||
offset,
|
||||
@@ -305,7 +305,7 @@ impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
if frame.id() != self.renderer_id {
|
||||
if frame.context_id() != self.renderer_context_id {
|
||||
warn!("trying to render texture from different renderer");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ macro_rules! niri_render_elements {
|
||||
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
|
||||
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
|
||||
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
|
||||
$($variant($type)),+
|
||||
|
||||
@@ -245,6 +245,11 @@ impl ShaderRenderElement {
|
||||
self.area.loc = location;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
self.alpha = alpha;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ShaderRenderElement {
|
||||
|
||||
@@ -175,6 +175,11 @@ impl ShadowRenderElement {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
self.inner = self.inner.with_alpha(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
|
||||
Shaders::get(renderer)
|
||||
.program(ProgramType::Shadow)
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn render_snapshot_from_surface_tree(
|
||||
}
|
||||
|
||||
let data = data.lock().unwrap();
|
||||
let Some(texture) = data.texture::<GlesRenderer>(renderer.id()) else {
|
||||
let Some(texture) = data.texture(renderer.context_id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::GlesTexture;
|
||||
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
|
||||
use smithay::backend::renderer::{Frame as _, ImportMem, Renderer, Texture};
|
||||
use smithay::backend::renderer::{ContextId, Frame as _, ImportMem, Renderer, Texture};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::memory::MemoryBuffer;
|
||||
|
||||
/// Smithay's texture buffer, but with fractional scale.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureBuffer<T> {
|
||||
pub struct TextureBuffer<T: Texture> {
|
||||
id: Id,
|
||||
commit_counter: CommitCounter,
|
||||
renderer_id: usize,
|
||||
renderer_context_id: ContextId<T>,
|
||||
texture: T,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
@@ -21,7 +21,7 @@ pub struct TextureBuffer<T> {
|
||||
|
||||
/// Render element for a [`TextureBuffer`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureRenderElement<T> {
|
||||
pub struct TextureRenderElement<T: Texture> {
|
||||
buffer: TextureBuffer<T>,
|
||||
location: Point<f64, Logical>,
|
||||
alpha: f32,
|
||||
@@ -30,7 +30,7 @@ pub struct TextureRenderElement<T> {
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
impl<T> TextureBuffer<T> {
|
||||
impl<T: Texture> TextureBuffer<T> {
|
||||
pub fn from_texture<R: Renderer<TextureId = T>>(
|
||||
renderer: &R,
|
||||
texture: T,
|
||||
@@ -41,7 +41,7 @@ impl<T> TextureBuffer<T> {
|
||||
TextureBuffer {
|
||||
id: Id::new(),
|
||||
commit_counter: CommitCounter::default(),
|
||||
renderer_id: renderer.id(),
|
||||
renderer_context_id: renderer.context_id(),
|
||||
texture,
|
||||
scale: scale.into(),
|
||||
transform,
|
||||
@@ -122,7 +122,7 @@ impl TextureBuffer<GlesTexture> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TextureRenderElement<T> {
|
||||
impl<T: Texture> TextureRenderElement<T> {
|
||||
pub fn from_texture_buffer(
|
||||
buffer: TextureBuffer<T>,
|
||||
location: impl Into<Point<f64, Logical>>,
|
||||
@@ -226,7 +226,7 @@ where
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), R::Error> {
|
||||
if frame.id() != self.buffer.renderer_id {
|
||||
if frame.context_id() != self.buffer.renderer_context_id {
|
||||
warn!("trying to render texture from different renderer");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ mod server;
|
||||
|
||||
mod floating;
|
||||
mod fullscreen;
|
||||
mod transactions;
|
||||
mod window_opening;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use niri_ipc::SizeChange;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
|
||||
use super::client::ClientId;
|
||||
use super::*;
|
||||
use crate::layout::LayoutElement;
|
||||
use crate::niri::Niri;
|
||||
|
||||
fn format_window_sizes(niri: &Niri) -> String {
|
||||
let mut buf = String::new();
|
||||
for (_out, mapped) in niri.layout.windows() {
|
||||
let size = mapped.size();
|
||||
writeln!(&mut buf, "{} × {}", size.w, size.h).unwrap();
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn create_window(f: &mut Fixture, id: ClientId, w: u16, h: u16) -> WlSurface {
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(w, h);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
surface
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_resize_waits_for_both_windows() {
|
||||
let mut f = Fixture::new();
|
||||
f.add_output(1, (1920, 1080));
|
||||
let id = f.add_client();
|
||||
|
||||
let surface1 = create_window(&mut f, id, 100, 100);
|
||||
let surface2 = create_window(&mut f, id, 200, 200);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface1).recent_configures();
|
||||
let _ = f.client(id).window(&surface2).recent_configures();
|
||||
|
||||
// Consume into one column.
|
||||
f.niri().layout.consume_or_expel_window_left(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Commit for the column consume.
|
||||
let window = f.client(id).window(&surface1);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 936 × 516, bounds: 1888 × 1048, states: []"
|
||||
);
|
||||
window.ack_last_and_commit();
|
||||
|
||||
let window = f.client(id).window(&surface2);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 936 × 516, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
window.ack_last_and_commit();
|
||||
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should say 100 × 100 and 200 × 200.
|
||||
assert_snapshot!(format_window_sizes(f.niri()), @r"
|
||||
100 × 100
|
||||
200 × 200
|
||||
");
|
||||
|
||||
// Issue a resize.
|
||||
f.niri()
|
||||
.layout
|
||||
.set_column_width(SizeChange::AdjustFixed(10));
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Commit window 1 in response to resize.
|
||||
let window = f.client(id).window(&surface1);
|
||||
window.set_size(300, 300);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should still say 100 × 100 as we're waiting in a transaction for the second window.
|
||||
assert_snapshot!(format_window_sizes(f.niri()), @r"
|
||||
100 × 100
|
||||
200 × 200
|
||||
");
|
||||
|
||||
// Commit window 2 in response to resize.
|
||||
let window = f.client(id).window(&surface2);
|
||||
window.set_size(400, 400);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should say 300 × 300 and 400 × 400 as the transaction completed.
|
||||
assert_snapshot!(format_window_sizes(f.niri()), @r"
|
||||
300 × 300
|
||||
400 × 400
|
||||
");
|
||||
}
|
||||
+14
-12
@@ -211,33 +211,33 @@ fn render(
|
||||
]);
|
||||
|
||||
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
|
||||
if binds
|
||||
if let Some(bind) = binds
|
||||
.iter()
|
||||
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
|
||||
.find(|bind| matches!(bind.action, Action::MoveColumnToWorkspaceDown(_)))
|
||||
{
|
||||
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||
actions.push(&bind.action);
|
||||
} else if binds
|
||||
.iter()
|
||||
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
|
||||
.any(|bind| matches!(bind.action, Action::MoveWindowToWorkspaceDown))
|
||||
{
|
||||
actions.push(&Action::MoveWindowToWorkspaceDown);
|
||||
} else {
|
||||
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||
actions.push(&Action::MoveColumnToWorkspaceDown(true));
|
||||
}
|
||||
|
||||
// Same for -up.
|
||||
if binds
|
||||
if let Some(bind) = binds
|
||||
.iter()
|
||||
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
|
||||
.find(|bind| matches!(bind.action, Action::MoveColumnToWorkspaceUp(_)))
|
||||
{
|
||||
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||
actions.push(&bind.action);
|
||||
} else if binds
|
||||
.iter()
|
||||
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
|
||||
.any(|bind| matches!(bind.action, Action::MoveWindowToWorkspaceUp))
|
||||
{
|
||||
actions.push(&Action::MoveWindowToWorkspaceUp);
|
||||
} else {
|
||||
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||
actions.push(&Action::MoveColumnToWorkspaceUp(true));
|
||||
}
|
||||
|
||||
actions.extend(&[
|
||||
@@ -247,6 +247,7 @@ fn render(
|
||||
&Action::ConsumeOrExpelWindowRight,
|
||||
&Action::ToggleWindowFloating,
|
||||
&Action::SwitchFocusBetweenFloatingAndTiling,
|
||||
&Action::ToggleOverview,
|
||||
]);
|
||||
|
||||
// Screenshot is not as important, can omit if not bound.
|
||||
@@ -423,8 +424,8 @@ fn action_name(action: &Action) -> String {
|
||||
Action::MoveColumnRight => String::from("Move Column Right"),
|
||||
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
|
||||
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
|
||||
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
|
||||
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
|
||||
Action::MoveColumnToWorkspaceDown(_) => String::from("Move Column to Workspace Down"),
|
||||
Action::MoveColumnToWorkspaceUp(_) => String::from("Move Column to Workspace Up"),
|
||||
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
|
||||
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
|
||||
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
|
||||
@@ -435,6 +436,7 @@ fn action_name(action: &Action) -> String {
|
||||
Action::SwitchFocusBetweenFloatingAndTiling => {
|
||||
String::from("Switch Focus Between Floating and Tiling")
|
||||
}
|
||||
Action::ToggleOverview => String::from("Open the Overview"),
|
||||
Action::Screenshot(_) => String::from("Take a Screenshot"),
|
||||
Action::Spawn(args) => format!(
|
||||
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
|
||||
|
||||
+206
-51
@@ -1,6 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::TAU;
|
||||
use std::iter::zip;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -11,14 +12,14 @@ use niri_ipc::SizeChange;
|
||||
use pango::{Alignment, FontDescription};
|
||||
use pangocairo::cairo::{self, ImageSurface};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::{ButtonState, MouseButton};
|
||||
use smithay::backend::input::TouchSlot;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{ExportMem, Texture as _};
|
||||
use smithay::input::keyboard::{Keysym, ModifiersState};
|
||||
use smithay::output::{Output, WeakOutput};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::layout::floating::DIRECTIONAL_MOVE_PX;
|
||||
@@ -32,6 +33,7 @@ use crate::utils::to_physical_precise_round;
|
||||
const SELECTION_BORDER: i32 = 2;
|
||||
|
||||
const PADDING: i32 = 8;
|
||||
const RADIUS: i32 = 16;
|
||||
const FONT: &str = "sans 14px";
|
||||
const BORDER: i32 = 4;
|
||||
const TEXT_HIDE_P: &str =
|
||||
@@ -56,7 +58,7 @@ pub enum ScreenshotUi {
|
||||
Open {
|
||||
selection: (Output, Point<i32, Physical>, Point<i32, Physical>),
|
||||
output_data: HashMap<Output, OutputData>,
|
||||
mouse_down: bool,
|
||||
button: Button,
|
||||
show_pointer: bool,
|
||||
open_anim: Animation,
|
||||
clock: Clock,
|
||||
@@ -64,6 +66,15 @@ pub enum ScreenshotUi {
|
||||
},
|
||||
}
|
||||
|
||||
pub enum Button {
|
||||
Up,
|
||||
Down {
|
||||
touch_slot: Option<TouchSlot>,
|
||||
on_capture_button: bool,
|
||||
last_pos: (Output, Point<i32, Physical>),
|
||||
},
|
||||
}
|
||||
|
||||
pub struct OutputData {
|
||||
size: Size<i32, Physical>,
|
||||
scale: f64,
|
||||
@@ -88,6 +99,22 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
fn is_down(&self) -> bool {
|
||||
matches!(self, Self::Down { .. })
|
||||
}
|
||||
|
||||
fn is_dragging_selection(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Down {
|
||||
on_capture_button: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenshotUi {
|
||||
pub fn new(clock: Clock, config: Rc<RefCell<Config>>) -> Self {
|
||||
Self::Closed {
|
||||
@@ -193,7 +220,7 @@ impl ScreenshotUi {
|
||||
*self = Self::Open {
|
||||
selection,
|
||||
output_data,
|
||||
mouse_down: false,
|
||||
button: Button::Up,
|
||||
show_pointer,
|
||||
open_anim,
|
||||
clock: clock.clone(),
|
||||
@@ -489,7 +516,7 @@ impl ScreenshotUi {
|
||||
let Self::Open {
|
||||
output_data,
|
||||
show_pointer,
|
||||
mouse_down,
|
||||
button,
|
||||
open_anim,
|
||||
..
|
||||
} = self
|
||||
@@ -509,17 +536,15 @@ impl ScreenshotUi {
|
||||
// The help panel goes on top.
|
||||
if let Some((show, hide)) = &output_data.panel {
|
||||
let buffer = if *show_pointer { hide } else { show };
|
||||
|
||||
let size = buffer.texture().size();
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
let x = max(0, (output_data.size.w - size.w) / 2);
|
||||
let y = max(0, output_data.size.h - size.h - padding * 2);
|
||||
let location = Point::<_, Physical>::from((x, y))
|
||||
let alpha = if button.is_dragging_selection() {
|
||||
0.3
|
||||
} else {
|
||||
0.9
|
||||
};
|
||||
let location = panel_location(output_data, buffer.texture().size())
|
||||
.to_f64()
|
||||
.to_logical(scale);
|
||||
|
||||
let alpha = if *mouse_down { 0.3 } else { 0.9 };
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
location,
|
||||
@@ -660,76 +685,155 @@ impl ScreenshotUi {
|
||||
}
|
||||
|
||||
/// The pointer has moved to `point` relative to the current selection output.
|
||||
pub fn pointer_motion(&mut self, point: Point<i32, Physical>) {
|
||||
pub fn pointer_motion(&mut self, point: Point<i32, Physical>, slot: Option<TouchSlot>) {
|
||||
let Self::Open {
|
||||
selection,
|
||||
mouse_down: true,
|
||||
button:
|
||||
Button::Down {
|
||||
touch_slot,
|
||||
on_capture_button,
|
||||
last_pos,
|
||||
},
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if *touch_slot != slot {
|
||||
return;
|
||||
}
|
||||
|
||||
last_pos.1 = point;
|
||||
|
||||
if *on_capture_button {
|
||||
return;
|
||||
}
|
||||
|
||||
selection.2 = point;
|
||||
self.update_buffers();
|
||||
}
|
||||
|
||||
pub fn pointer_button(
|
||||
pub fn pointer_down(
|
||||
&mut self,
|
||||
output: Output,
|
||||
point: Point<i32, Physical>,
|
||||
button: MouseButton,
|
||||
state: ButtonState,
|
||||
slot: Option<TouchSlot>,
|
||||
) -> bool {
|
||||
let Self::Open {
|
||||
selection,
|
||||
output_data,
|
||||
mouse_down,
|
||||
show_pointer,
|
||||
button,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if button != MouseButton::Left {
|
||||
if button.is_down() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let down = state == ButtonState::Pressed;
|
||||
if *mouse_down == down {
|
||||
let Some(output_data) = output_data.get(&output) else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if down && !output_data.contains_key(&output) {
|
||||
return false;
|
||||
}
|
||||
if let Some((show, hide)) = &output_data.panel {
|
||||
let buffer = if *show_pointer { hide } else { show };
|
||||
let panel_size = buffer.texture().size();
|
||||
let location = panel_location(output_data, panel_size);
|
||||
|
||||
*mouse_down = down;
|
||||
|
||||
if down {
|
||||
*selection = (output, point, point);
|
||||
} else {
|
||||
// Check if the resulting selection is zero-sized, and try to come up with a small
|
||||
// default rectangle.
|
||||
let (output, a, b) = selection;
|
||||
let mut rect = rect_from_corner_points(*a, *b);
|
||||
if rect.size.is_empty() || rect.size == Size::from((1, 1)) {
|
||||
let data = &output_data[output];
|
||||
rect = Rectangle::new(
|
||||
Point::from((rect.loc.x - 16, rect.loc.y - 16)),
|
||||
Size::from((32, 32)),
|
||||
)
|
||||
.intersection(Rectangle::from_size(data.size))
|
||||
.unwrap_or_default();
|
||||
*a = rect.loc;
|
||||
*b = rect.loc + rect.size - Size::from((1, 1));
|
||||
if is_within_capture_button(output_data.scale, panel_size, point - location) {
|
||||
*button = Button::Down {
|
||||
touch_slot: slot,
|
||||
on_capture_button: true,
|
||||
last_pos: (output, point),
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
*button = Button::Down {
|
||||
touch_slot: slot,
|
||||
on_capture_button: false,
|
||||
last_pos: (output.clone(), point),
|
||||
};
|
||||
*selection = (output, point, point);
|
||||
|
||||
self.update_buffers();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn pointer_up(&mut self, slot: Option<TouchSlot>) -> Option<bool> {
|
||||
let Self::Open {
|
||||
selection,
|
||||
output_data,
|
||||
button,
|
||||
show_pointer,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Button::Down {
|
||||
touch_slot,
|
||||
on_capture_button,
|
||||
ref last_pos,
|
||||
} = *button
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if touch_slot != slot {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_pos = last_pos.clone();
|
||||
*button = Button::Up;
|
||||
|
||||
// Check if we released still on the capture button.
|
||||
if on_capture_button {
|
||||
let (output, point) = last_pos;
|
||||
|
||||
#[allow(clippy::question_mark)]
|
||||
let Some(output_data) = output_data.get(&output) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some((show, hide)) = &output_data.panel {
|
||||
let buffer = if *show_pointer { hide } else { show };
|
||||
let panel_size = buffer.texture().size();
|
||||
let location = panel_location(output_data, panel_size);
|
||||
|
||||
if is_within_capture_button(output_data.scale, panel_size, point - location) {
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the resulting selection is zero-sized, and try to come up with a small
|
||||
// default rectangle.
|
||||
let (output, a, b) = selection;
|
||||
let mut rect = rect_from_corner_points(*a, *b);
|
||||
if rect.size.is_empty() || rect.size == Size::from((1, 1)) {
|
||||
let data = &output_data[output];
|
||||
rect = Rectangle::new(
|
||||
Point::from((rect.loc.x - 16, rect.loc.y - 16)),
|
||||
Size::from((32, 32)),
|
||||
)
|
||||
.intersection(Rectangle::from_size(data.size))
|
||||
.unwrap_or_default();
|
||||
*a = rect.loc;
|
||||
*b = rect.loc + rect.size - Size::from((1, 1));
|
||||
}
|
||||
|
||||
self.update_buffers();
|
||||
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputScreenshot {
|
||||
@@ -819,6 +923,29 @@ pub fn rect_from_corner_points(
|
||||
Rectangle::from_extremities((x1, y1), (x2 + 1, y2 + 1))
|
||||
}
|
||||
|
||||
fn panel_location(output_data: &OutputData, panel_size: Size<i32, Buffer>) -> Point<i32, Physical> {
|
||||
let scale = output_data.scale;
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
let x = max(0, (output_data.size.w - panel_size.w) / 2);
|
||||
let y = max(0, output_data.size.h - panel_size.h - padding * 2);
|
||||
Point::from((x, y))
|
||||
}
|
||||
|
||||
fn is_within_capture_button(
|
||||
scale: f64,
|
||||
panel_size: Size<i32, Buffer>,
|
||||
pos_within_panel: Point<i32, Physical>,
|
||||
) -> bool {
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
let radius = to_physical_precise_round::<i32>(scale, RADIUS) - 2;
|
||||
|
||||
let xc = padding + radius;
|
||||
let yc = panel_size.h / 2;
|
||||
let pos = pos_within_panel;
|
||||
|
||||
(pos.x - xc) * (pos.x - xc) + (pos.y - yc) * (pos.y - yc) <= radius * radius
|
||||
}
|
||||
|
||||
fn render_panel(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: f64,
|
||||
@@ -827,6 +954,11 @@ fn render_panel(
|
||||
let _span = tracy_client::span!("screenshot_ui::render_panel");
|
||||
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
// Keep the border width even to avoid blurry edges.
|
||||
let border_width = (f64::from(BORDER) / 2. * scale).round() * 2.;
|
||||
let half_border_width = (border_width / 2.) as i32;
|
||||
let radius: i32 = to_physical_precise_round(scale, RADIUS);
|
||||
let circle_stroke: f64 = to_physical_precise_round(scale, 2.);
|
||||
|
||||
// Add 2 px of spacing to separate the backgrounds of the "Space" and "P" keys.
|
||||
let spacing = to_physical_precise_round::<i32>(scale, 2) * 1024;
|
||||
@@ -839,12 +971,14 @@ fn render_panel(
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_alignment(Alignment::Left);
|
||||
layout.set_markup(text);
|
||||
layout.set_spacing(spacing);
|
||||
|
||||
let (mut width, mut height) = layout.pixel_size();
|
||||
width += padding * 2;
|
||||
|
||||
width += padding + radius * 2 + padding - half_border_width + padding;
|
||||
height = max(height, radius * 2);
|
||||
height += padding * 2;
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
|
||||
@@ -852,11 +986,33 @@ fn render_panel(
|
||||
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let padding = f64::from(padding);
|
||||
let half_border_width = f64::from(half_border_width);
|
||||
let r = f64::from(radius);
|
||||
|
||||
let yc = f64::from(height / 2);
|
||||
|
||||
cr.new_sub_path();
|
||||
cr.arc(padding + r, yc, r, 0., TAU);
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
cr.fill()?;
|
||||
|
||||
cr.new_sub_path();
|
||||
cr.arc(padding + r, yc, r - circle_stroke, 0., TAU);
|
||||
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||
cr.fill()?;
|
||||
|
||||
cr.new_sub_path();
|
||||
cr.arc(padding + r, yc, r - circle_stroke * 2., 0., TAU);
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
cr.fill()?;
|
||||
|
||||
cr.move_to(padding + r * 2. + padding - half_border_width, padding);
|
||||
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_alignment(Alignment::Left);
|
||||
layout.set_markup(text);
|
||||
layout.set_spacing(spacing);
|
||||
|
||||
@@ -869,8 +1025,7 @@ fn render_panel(
|
||||
cr.line_to(0., height.into());
|
||||
cr.line_to(0., 0.);
|
||||
cr.set_source_rgb(0.3, 0.3, 0.3);
|
||||
// Keep the border width even to avoid blurry edges.
|
||||
cr.set_line_width((f64::from(BORDER) / 2. * scale).round() * 2.);
|
||||
cr.set_line_width(border_width);
|
||||
cr.stroke()?;
|
||||
drop(cr);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::f64;
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
@@ -396,6 +397,12 @@ pub fn center_preferring_top_left_in_area(
|
||||
area.loc + offset
|
||||
}
|
||||
|
||||
pub fn baba_is_float_offset(now: Duration, view_height: f64) -> f64 {
|
||||
let now = now.as_secs_f64();
|
||||
let amplitude = view_height / 96.;
|
||||
amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub fn show_screenshot_notification(image_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -93,6 +93,8 @@ impl Transaction {
|
||||
let _span = trace_span!("deadline timer", transaction = ?Weak::as_ptr(&inner))
|
||||
.entered();
|
||||
|
||||
// FIXME: come up with some way to control the deadline timer from tests.
|
||||
#[cfg(not(test))]
|
||||
if let Some(inner) = inner.upgrade() {
|
||||
trace!("deadline reached, completing transaction");
|
||||
inner.complete();
|
||||
|
||||
@@ -77,6 +77,9 @@ pub struct Mapped {
|
||||
/// If `None`, then the window is not offscreened.
|
||||
offscreen_data: RefCell<Option<OffscreenData>>,
|
||||
|
||||
/// Whether this has an urgent indicator.
|
||||
is_urgent: bool,
|
||||
|
||||
/// Whether this window has the keyboard focus.
|
||||
is_focused: bool,
|
||||
|
||||
@@ -231,6 +234,7 @@ impl Mapped {
|
||||
needs_configure: false,
|
||||
needs_frame_callback: false,
|
||||
offscreen_data: RefCell::new(None),
|
||||
is_urgent: false,
|
||||
is_focused: false,
|
||||
is_active_in_column: true,
|
||||
is_floating: false,
|
||||
@@ -328,6 +332,7 @@ impl Mapped {
|
||||
}
|
||||
|
||||
self.is_focused = is_focused;
|
||||
self.is_urgent = false;
|
||||
self.need_to_recompute_rules = true;
|
||||
}
|
||||
|
||||
@@ -510,6 +515,20 @@ impl Mapped {
|
||||
pub fn is_windowed_fullscreen(&self) -> bool {
|
||||
self.is_windowed_fullscreen
|
||||
}
|
||||
|
||||
pub fn set_urgent(&mut self, urgent: bool) {
|
||||
if self.is_focused && urgent {
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = self.is_urgent != urgent;
|
||||
self.is_urgent = urgent;
|
||||
self.need_to_recompute_rules |= changed;
|
||||
}
|
||||
|
||||
pub fn is_urgent(&self) -> bool {
|
||||
self.is_urgent
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Mapped {
|
||||
@@ -830,6 +849,10 @@ impl LayoutElement for Mapped {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_urgent(&self) -> bool {
|
||||
self.is_urgent
|
||||
}
|
||||
|
||||
fn set_activated(&mut self, active: bool) {
|
||||
let changed = self.toplevel().with_pending_state(|state| {
|
||||
if active {
|
||||
|
||||
+20
-1
@@ -100,7 +100,7 @@ pub struct ResolvedWindowRules {
|
||||
/// Whether to clip this window to its geometry, including the corner radius.
|
||||
pub clip_to_geometry: Option<bool>,
|
||||
|
||||
/// Whether bob this window up and down.
|
||||
/// Whether to bob this window up and down.
|
||||
pub baba_is_float: Option<bool>,
|
||||
|
||||
/// Whether to block out this window from certain render targets.
|
||||
@@ -131,6 +131,13 @@ impl<'a> WindowRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_urgent(self) -> bool {
|
||||
match self {
|
||||
WindowRef::Unmapped(_) => false,
|
||||
WindowRef::Mapped(mapped) => mapped.is_urgent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active_in_column(self) -> bool {
|
||||
match self {
|
||||
WindowRef::Unmapped(_) => true,
|
||||
@@ -184,8 +191,10 @@ impl ResolvedWindowRules {
|
||||
width: None,
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
border: BorderRule {
|
||||
off: false,
|
||||
@@ -193,8 +202,10 @@ impl ResolvedWindowRules {
|
||||
width: None,
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
@@ -209,8 +220,10 @@ impl ResolvedWindowRules {
|
||||
tab_indicator: TabIndicatorRule {
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
draw_border_with_background: None,
|
||||
opacity: None,
|
||||
@@ -427,6 +440,12 @@ fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m:
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(is_urgent) = m.is_urgent {
|
||||
if window.is_urgent() != is_urgent {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(is_active) = m.is_active {
|
||||
// Our "is-active" definition corresponds to the window having a pending Activated state.
|
||||
let pending_activated = server_pending
|
||||
|
||||
@@ -77,6 +77,8 @@ On some systems, Steam will show a fully black window.
|
||||
To fix this, navigate to Settings -> Interface (via Steam's tray icon, or by blindly finding the Steam menu at the top left of the window), then **disable** GPU accelerated rendering in web views.
|
||||
Restart Steam and it should now work fine.
|
||||
|
||||
If you do not want to disable GPU accelerated rendering you can instead try to pass the launch argument `-system-composer` instead.
|
||||
|
||||
Steam notifications don't run through the standard notification daemon and show up as floating windows in the center of the screen.
|
||||
You can move them to a more convenient location by adding a window rule in your niri config:
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ animations {
|
||||
duration-ms 200
|
||||
curve "ease-out-quad"
|
||||
}
|
||||
|
||||
overview-open-close {
|
||||
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,7 +163,7 @@ animations {
|
||||
|
||||
##### `custom-shader`
|
||||
|
||||
<sup>Since: 0.1.6, experimental</sup>
|
||||
<sup>Since: 0.1.6</sup>
|
||||
|
||||
You can write a custom shader for drawing the window during an open animation.
|
||||
|
||||
@@ -219,7 +223,7 @@ animations {
|
||||
|
||||
##### `custom-shader`
|
||||
|
||||
<sup>Since: 0.1.6, experimental</sup>
|
||||
<sup>Since: 0.1.6</sup>
|
||||
|
||||
You can write a custom shader for drawing the window during a close animation.
|
||||
|
||||
@@ -315,7 +319,7 @@ animations {
|
||||
|
||||
##### `custom-shader`
|
||||
|
||||
<sup>Since: 0.1.6, experimental</sup>
|
||||
<sup>Since: 0.1.6</sup>
|
||||
|
||||
You can write a custom shader for drawing the window during a resize animation.
|
||||
|
||||
@@ -374,6 +378,20 @@ animations {
|
||||
}
|
||||
```
|
||||
|
||||
#### `overview-open-close`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
The open/close zoom animation of the [Overview](./Overview.md).
|
||||
|
||||
```kdl
|
||||
animations {
|
||||
overview-open-close {
|
||||
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Synchronized Animations
|
||||
|
||||
<sup>Since: 0.1.5</sup>
|
||||
|
||||
@@ -4,7 +4,7 @@ Niri has several options that are only useful for debugging, or are experimental
|
||||
They are not meant for normal use.
|
||||
|
||||
> [!CAUTION]
|
||||
> These options are **not** covered by the [config breaking change policy](./Configuration:-Overview.md#breaking-change-policy).
|
||||
> These options are **not** covered by the [config breaking change policy](./Configuration:-Introduction.md#breaking-change-policy).
|
||||
> They can change or stop working at any point with little notice.
|
||||
|
||||
Here are all the options at a glance:
|
||||
@@ -155,7 +155,7 @@ debug {
|
||||
|
||||
### `wait-for-frame-completion-in-pipewire`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Wait until every screencast frame is done rendering before handing it over to PipeWire.
|
||||
|
||||
@@ -258,7 +258,7 @@ debug {
|
||||
|
||||
### `honor-xdg-activation-with-invalid-serial`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Widely-used clients such as Discord and Telegram make fresh xdg-activation tokens upon clicking on their tray icon or on their notification.
|
||||
Most of the time, these fresh tokens will have invalid serials, because the app needs to be focused to get a valid serial, and if the user clicks on a tray icon or a notification, it is usually because the app *isn't* focused, and the user wants to focus it.
|
||||
|
||||
@@ -14,6 +14,16 @@ gestures {
|
||||
delay-ms 100
|
||||
max-speed 1500
|
||||
}
|
||||
|
||||
dnd-edge-workspace-switch {
|
||||
trigger-height 50
|
||||
delay-ms 100
|
||||
max-speed 1500
|
||||
}
|
||||
|
||||
hot-corners {
|
||||
// off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,3 +51,46 @@ gestures {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `dnd-edge-workspace-switch`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Scroll the workspaces up/down when moving the mouse cursor against a monitor edge during drag-and-drop (DnD) while in the overview.
|
||||
Also works on a touchscreen.
|
||||
|
||||
The options are:
|
||||
|
||||
- `trigger-height`: size of the area near the monitor edge that will trigger the scrolling, in logical pixels.
|
||||
- `delay-ms`: delay in milliseconds before the scrolling starts.
|
||||
Avoids unwanted scrolling when dragging things across monitors.
|
||||
- `max-speed`: maximum scrolling speed; 1500 corresponds to one screen height per second.
|
||||
The scrolling speed increases linearly as you move your mouse cursor from `trigger-width` to the very edge of the monitor.
|
||||
|
||||
```kdl
|
||||
gestures {
|
||||
// Increase the trigger area and maximum speed.
|
||||
dnd-edge-workspace-switch {
|
||||
trigger-height 100
|
||||
max-speed 3000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `hot-corners`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Put your mouse at the very top-left corner of a monitor to toggle the overview.
|
||||
Also works during drag-and-dropping something.
|
||||
|
||||
`off` disables the hot corners.
|
||||
|
||||
```kdl
|
||||
// Disable the hot corners.
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,6 +23,7 @@ input {
|
||||
// repeat-delay 600
|
||||
// repeat-rate 25
|
||||
// track-layout "global"
|
||||
numlock
|
||||
}
|
||||
|
||||
touchpad {
|
||||
@@ -166,6 +167,24 @@ input {
|
||||
}
|
||||
```
|
||||
|
||||
#### Num Lock
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Set the `numlock` flag to turn on Num Lock automatically at startup.
|
||||
|
||||
You might want to disable (comment out) `numlock` if you're using a laptop with a keyboard that overlays Num Lock keys on top of regular keys.
|
||||
|
||||
Note that there's a [known issue](https://github.com/YaLTeR/niri/issues/1501) with this setting: Num Lock only turns on after you press some modifier key (Super, Alt, etc.).
|
||||
|
||||
```kdl
|
||||
input {
|
||||
keyboard {
|
||||
numlock
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pointing Devices
|
||||
|
||||
Most settings for the pointing devices are passed directly to libinput.
|
||||
@@ -192,7 +211,7 @@ Settings specific to `touchpad`s:
|
||||
- `tap`: tap-to-click.
|
||||
- `dwt`: disable-when-typing.
|
||||
- `dwtp`: disable-when-trackpointing.
|
||||
- `drag`: can be `true` or `false`, controls if tap-and-drag is enabled.
|
||||
- `drag`: <sup>Since: 25.05</sup> can be `true` or `false`, controls if tap-and-drag is enabled.
|
||||
- `drag-lock`: <sup>Since: 25.02</sup> if set, lifting the finger off for a short time while dragging will not drop the dragged item. See the [libinput documentation](https://wayland.freedesktop.org/libinput/doc/latest/tapping.html#tap-and-drag).
|
||||
- `tap-button-map`: can be `left-right-middle` or `left-middle-right`, controls which button corresponds to a two-finger tap and a three-finger tap.
|
||||
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
|
||||
@@ -254,7 +273,7 @@ input {
|
||||
By default, the cursor warps *separately* horizontally and vertically.
|
||||
I.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then the mouse will move only horizontally, and not vertically.
|
||||
|
||||
<sup>Since: next release</sup> You can customize this with the `mode` property.
|
||||
<sup>Since: 25.05</sup> You can customize this with the `mode` property.
|
||||
|
||||
- `mode="center-xy"`: warps by both X and Y coordinates together.
|
||||
So if the mouse was anywhere outside the newly focused window, it will warp to the center of the window.
|
||||
@@ -309,7 +328,7 @@ input {
|
||||
|
||||
#### `mod-key`, `mod-key-nested`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Customize the `Mod` key for [key bindings](./Configuration:-Key-Bindings.md).
|
||||
Only valid modifiers are allowed, e.g. `Super`, `Alt`, `Mod3`, `Mod5`, `Ctrl`, `Shift`.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
### Per-Section Documentation
|
||||
|
||||
You can find documentation for various sections of the config on these wiki pages:
|
||||
|
||||
* [`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)
|
||||
* [`layer-rule {}`](./Configuration:-Layer-Rules.md)
|
||||
* [`animations {}`](./Configuration:-Animations.md)
|
||||
* [`gestures {}`](./Configuration:-Gestures.md)
|
||||
* [`debug {}`](./Configuration:-Debug-Options.md)
|
||||
|
||||
### Loading
|
||||
|
||||
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
Please use the default configuration file as the starting point for your custom configuration.
|
||||
|
||||
The configuration is live-reloaded.
|
||||
Simply edit and save the config file, and your changes will be applied.
|
||||
This includes key bindings, output settings like mode, window rules, and everything else.
|
||||
|
||||
You can run `niri validate` to parse the config and see any errors.
|
||||
|
||||
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
|
||||
|
||||
You can also set `$NIRI_CONFIG` to the path of the config file.
|
||||
`--config` always takes precedence.
|
||||
If `--config` or `$NIRI_CONFIG` doesn't point to a real file, the config will not be loaded.
|
||||
If `$NIRI_CONFIG` is set to an empty string, it is ignored and the default config location is used instead.
|
||||
|
||||
### Syntax
|
||||
|
||||
The config is written in [KDL].
|
||||
|
||||
#### Comments
|
||||
|
||||
Lines starting with `//` are comments; they are ignored.
|
||||
|
||||
Also, you can put `/-` in front of a section to comment out the entire section:
|
||||
|
||||
```kdl
|
||||
/-output "eDP-1" {
|
||||
// Everything inside here is ignored.
|
||||
// The display won't be turned off
|
||||
// as the whole section is commented out.
|
||||
off
|
||||
}
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
Toggle options in niri are commonly represented as flags.
|
||||
Writing out the flag enables it, and omitting it or commenting it out disables it.
|
||||
For example:
|
||||
|
||||
```kdl
|
||||
// "Focus follows mouse" is enabled.
|
||||
input {
|
||||
focus-follows-mouse
|
||||
|
||||
// Other settings...
|
||||
}
|
||||
```
|
||||
|
||||
```kdl
|
||||
// "Focus follows mouse" is disabled.
|
||||
input {
|
||||
// focus-follows-mouse
|
||||
|
||||
// Other settings...
|
||||
}
|
||||
```
|
||||
|
||||
#### Sections
|
||||
|
||||
Most sections cannot be repeated. For example:
|
||||
|
||||
```kdl
|
||||
// This is valid: every section appears once.
|
||||
input {
|
||||
keyboard {
|
||||
// ...
|
||||
}
|
||||
|
||||
touchpad {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kdl,must-fail
|
||||
// This is NOT valid: input section appears twice.
|
||||
input {
|
||||
keyboard {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touchpad {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exceptions are, for example, sections that configure different devices by name:
|
||||
|
||||
<!-- NOTE: this may break in the future -->
|
||||
```kdl
|
||||
output "eDP-1" {
|
||||
// ...
|
||||
}
|
||||
|
||||
// This is valid: this section configures a different output.
|
||||
output "HDMI-A-1" {
|
||||
// ...
|
||||
}
|
||||
|
||||
// This is NOT valid: "eDP-1" already appeared above.
|
||||
// It will either throw a config parsing error, or otherwise not work.
|
||||
output "eDP-1" {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Defaults
|
||||
|
||||
Omitting most of the sections of the config file will leave you with the default values for that section.
|
||||
A notable exception is [`binds {}`](./Configuration:-Key-Bindings.md): they do not get filled with defaults, so make sure you do not erase this section.
|
||||
|
||||
### Breaking Change Policy
|
||||
|
||||
As a rule, niri updates should not break existing config files.
|
||||
(For example, the default config from niri v0.1.0 still parses fine on v25.02 as I'm writing this.)
|
||||
|
||||
Exceptions can be made for parsing bugs.
|
||||
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
|
||||
A patch release changed niri from silently accepting this to causing a parsing failure.
|
||||
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
|
||||
|
||||
Keep in mind that the breaking change policy applies only to niri releases.
|
||||
Commits between releases can and do occasionally break the config as new features are ironed out.
|
||||
However, I do try to limit these, since several people are running git builds.
|
||||
|
||||
[KDL]: https://kdl.dev/
|
||||
@@ -31,7 +31,7 @@ Valid modifiers are:
|
||||
This way, you can test niri in a window without causing too many conflicts with the host compositor's key bindings.
|
||||
For this reason, most of the default keys use the `Mod` modifier.
|
||||
|
||||
<sup>Since: next release</sup> You can customize the `Mod` key [in the `input` section of the config](./Configuration:-Input.md#mod-key-mod-key-nested).
|
||||
<sup>Since: 25.05</sup> You can customize the `Mod` key [in the `input` section of the config](./Configuration:-Input.md#mod-key-mod-key-nested).
|
||||
|
||||
> [!TIP]
|
||||
> To find an XKB name for a particular key, you may use a program like [`wev`](https://git.sr.ht/~sircmpwn/wev).
|
||||
@@ -52,6 +52,15 @@ For this reason, most of the default keys use the `Mod` modifier.
|
||||
>
|
||||
> Here, look at `sym: Left` and `sym: Right`: these are the key names.
|
||||
> I was pressing the left and the right arrow in this example.
|
||||
>
|
||||
> Keep in mind that binding shifted keys requires spelling out Shift and the unshifted version of the key, according to your XKB layout.
|
||||
> For example, on the US QWERTY layout, <kbd><</kbd> is on <kbd>Shift</kbd> + <kbd>,</kbd>, so to bind it, you spell out something like `Mod+Shift+Comma`.
|
||||
>
|
||||
> As another example, if you've configured the French [BÉPO](https://en.wikipedia.org/wiki/B%C3%89PO) XKB layout, your <kbd><</kbd> is on <kbd>AltGr</kbd> + <kbd>«</kbd>.
|
||||
> <kbd>AltGr</kbd> is `ISO_Level3_Shift`, or equivalently `Mod5`, so to bind it, you spell out something like `Mod+Mod5+guillemotleft`.
|
||||
>
|
||||
> When resolving latin keys, niri will search for the *first* configured XKB layout that has the latin key.
|
||||
> So for example with US QWERTY and RU layouts configured, US QWERTY will be used for latin binds.
|
||||
|
||||
<sup>Since: 0.1.8</sup> Binds will repeat by default (i.e. holding down a bind will make it trigger repeatedly).
|
||||
You can disable that for specific binds with `repeat=false`:
|
||||
@@ -328,7 +337,7 @@ binds {
|
||||
|
||||
In the interactive screenshot UI, pressing <kbd>Ctrl</kbd><kbd>C</kbd> will copy the screenshot to the clipboard without writing it to disk.
|
||||
|
||||
<sup>Since: next release</sup> You can hide the mouse pointer in screenshots with the `show-pointer=false` property:
|
||||
<sup>Since: 25.05</sup> You can hide the mouse pointer in screenshots with the `show-pointer=false` property:
|
||||
|
||||
```kdl
|
||||
binds {
|
||||
|
||||
@@ -32,6 +32,8 @@ layer-rule {
|
||||
}
|
||||
|
||||
geometry-corner-radius 12
|
||||
place-within-backdrop true
|
||||
baba-is-float true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -129,7 +131,7 @@ That is, enabling shadows in the layout config section won't automatically enabl
|
||||
// Add a shadow for fuzzel.
|
||||
layer-rule {
|
||||
match namespace="^launcher$"
|
||||
|
||||
|
||||
shadow {
|
||||
on
|
||||
}
|
||||
@@ -149,6 +151,43 @@ This setting will only affect the shadow—it will round its corners to match th
|
||||
|
||||
```kdl
|
||||
layer-rule {
|
||||
match namespace="^launcher$"
|
||||
|
||||
geometry-corner-radius 12
|
||||
}
|
||||
```
|
||||
|
||||
#### `place-within-backdrop`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Set to `true` to place the surface into the backdrop visible in the [Overview](./Overview.md) and between workspaces.
|
||||
|
||||
This will only work for *background* layer surfaces that ignore exclusive zones (typical for wallpaper tools).
|
||||
Layers within the backdrop will ignore all input.
|
||||
|
||||
```kdl
|
||||
// Put swaybg inside the overview backdrop.
|
||||
layer-rule {
|
||||
match namespace="^wallpaper$"
|
||||
|
||||
place-within-backdrop true
|
||||
}
|
||||
```
|
||||
|
||||
#### `baba-is-float`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Make your layer surfaces FLOAT up and down.
|
||||
|
||||
This is a natural extension of the [April Fools' 2025 feature](./Configuration:-Window-Rules.md#baba-is-float).
|
||||
|
||||
```kdl
|
||||
// Make fuzzel FLOAT.
|
||||
layer-rule {
|
||||
match namespace="^launcher$"
|
||||
|
||||
baba-is-float true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -11,6 +11,7 @@ layout {
|
||||
always-center-single-column
|
||||
empty-workspace-above-first
|
||||
default-column-display "tabbed"
|
||||
background-color "#003300"
|
||||
|
||||
preset-column-widths {
|
||||
proportion 0.33333
|
||||
@@ -31,8 +32,10 @@ layout {
|
||||
width 4
|
||||
active-color "#7fc8ff"
|
||||
inactive-color "#505050"
|
||||
urgent-color "#9b0000"
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
// urgent-gradient from="#800" to="#a33" angle=45
|
||||
}
|
||||
|
||||
border {
|
||||
@@ -40,8 +43,10 @@ layout {
|
||||
width 4
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
urgent-color "#9b0000"
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
|
||||
// urgent-gradient from="#800" to="#a33" angle=45
|
||||
}
|
||||
|
||||
shadow {
|
||||
@@ -66,8 +71,10 @@ layout {
|
||||
corner-radius 8
|
||||
active-color "red"
|
||||
inactive-color "gray"
|
||||
urgent-color "blue"
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
// urgent-gradient from="#800" to="#a33" angle=45
|
||||
}
|
||||
|
||||
insert-hint {
|
||||
@@ -269,6 +276,9 @@ layout {
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
|
||||
// Color of the border around windows that request your attention.
|
||||
urgent-color "#9b0000"
|
||||
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
|
||||
}
|
||||
@@ -372,7 +382,7 @@ Set `on` to enable the shadow.
|
||||
Setting `softness 0` will give you hard shadows.
|
||||
|
||||
`spread` is the distance to expand the window rectangle in logical pixels, same as CSS box-shadow spread.
|
||||
<sup>Since: next release</sup> Spread can be negative.
|
||||
<sup>Since: 25.05</sup> Spread can be negative.
|
||||
|
||||
`offset` moves the shadow relative to the window in logical pixels, same as CSS box-shadow offset.
|
||||
For example, `offset x=2 y=2` will move the shadow 2 logical pixels downwards and to the right.
|
||||
@@ -440,7 +450,7 @@ It can be `left`, `right`, `top`, or `bottom`.
|
||||
`corner-radius` sets the rounded corner radius for tabs in the indicator in logical pixels.
|
||||
When `gaps-between-tabs` is zero, only the first and the last tabs have rounded corners, otherwise all tabs do.
|
||||
|
||||
`active-color`, `inactive-color`, `active-gradient`, `inactive-gradient` let you override the colors for the tabs.
|
||||
`active-color`, `inactive-color`, `urgent-color`, `active-gradient`, `inactive-gradient`, `urgent-gradient` let you override the colors for the tabs.
|
||||
They have the same semantics as the border and focus ring colors and gradients.
|
||||
|
||||
Tab colors are picked in this order:
|
||||
@@ -526,3 +536,18 @@ layout {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `background-color`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Set the default background color that niri draws for workspaces.
|
||||
This is visible when you're not using any background tools like swaybg.
|
||||
|
||||
```kdl
|
||||
layout {
|
||||
background-color "#003300"
|
||||
}
|
||||
```
|
||||
|
||||
You can also set the color per-output [in the output config](./Configuration:-Outputs.md#background-color).
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
### Overview
|
||||
|
||||
This page documents all top-level options that don't otherwise have dedicated pages.
|
||||
|
||||
Here are all of these options at a glance:
|
||||
@@ -25,6 +23,19 @@ cursor {
|
||||
hide-after-inactive-ms 1000
|
||||
}
|
||||
|
||||
overview {
|
||||
zoom 0.5
|
||||
backdrop-color "#262626"
|
||||
|
||||
workspace-shadow {
|
||||
// off
|
||||
softness 40
|
||||
spread 10
|
||||
offset x=0 y=10
|
||||
color "#00000050"
|
||||
}
|
||||
}
|
||||
|
||||
clipboard {
|
||||
disable-primary
|
||||
}
|
||||
@@ -143,6 +154,58 @@ cursor {
|
||||
}
|
||||
```
|
||||
|
||||
### `overview`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Settings for the [Overview](./Overview.md).
|
||||
|
||||
#### `zoom`
|
||||
|
||||
Control how much the workspaces zoom out in the overview.
|
||||
`zoom` ranges from 0 to 0.75 where lower values make everything smaller.
|
||||
|
||||
```kdl
|
||||
// Make workspaces four times smaller than normal in the overview.
|
||||
overview {
|
||||
zoom 0.25
|
||||
}
|
||||
```
|
||||
|
||||
#### `backdrop-color`
|
||||
|
||||
Set the backdrop color behind workspaces in the overview.
|
||||
The backdrop is also visible between workspaces when switching.
|
||||
|
||||
The alpha channel for this color will be ignored.
|
||||
|
||||
```kdl
|
||||
// Make the backdrop light.
|
||||
overview {
|
||||
backdrop-color "#777777"
|
||||
}
|
||||
```
|
||||
|
||||
You can also set the color per-output [in the output config](./Configuration:-Outputs.md#backdrop-color).
|
||||
|
||||
#### `workspace-shadow`
|
||||
|
||||
Control the shadow behind workspaces visible in the overview.
|
||||
|
||||
Settings here mirror the normal [`shadow` config in the layout section](./Configuration:-Layout.md#shadow), so check the documentation there.
|
||||
|
||||
Workspace shadows are configured for a workspace size normalized to 1080 pixels tall, then zoomed out together with the workspace.
|
||||
Practically, this means that you'll want bigger spread, offset, and softness compared to window shadows.
|
||||
|
||||
```kdl
|
||||
// Disable workspace shadows in the overview.
|
||||
overview {
|
||||
workspace-shadow {
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `clipboard`
|
||||
|
||||
<sup>Since: 25.02</sup>
|
||||
|
||||
@@ -15,6 +15,7 @@ output "eDP-1" {
|
||||
variable-refresh-rate // on-demand=true
|
||||
focus-at-startup
|
||||
background-color "#003300"
|
||||
backdrop-color "#001100"
|
||||
}
|
||||
|
||||
output "HDMI-A-1" {
|
||||
@@ -167,7 +168,7 @@ output "HDMI-A-1" {
|
||||
|
||||
### `focus-at-startup`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Focus this output by default when niri starts.
|
||||
|
||||
@@ -191,13 +192,28 @@ output "DP-2" {
|
||||
|
||||
<sup>Since: 0.1.8</sup>
|
||||
|
||||
Set the background color that niri draws for this output.
|
||||
Set the background color that niri draws for workspaces on this output.
|
||||
This is visible when you're not using any background tools like swaybg.
|
||||
|
||||
The alpha channel for this color will be ignored.
|
||||
<sup>Until: 25.05</sup> The alpha channel for this color will be ignored.
|
||||
|
||||
```kdl
|
||||
output "HDMI-A-1" {
|
||||
background-color "#003300"
|
||||
}
|
||||
```
|
||||
|
||||
### `backdrop-color`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Set the backdrop color that niri draws for this output.
|
||||
This is visible between workspaces or in the overview.
|
||||
|
||||
The alpha channel for this color will be ignored.
|
||||
|
||||
```kdl
|
||||
output "HDMI-A-1" {
|
||||
backdrop-color "#001100"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,150 +1 @@
|
||||
### Per-Section Documentation
|
||||
|
||||
You can find documentation for various sections of the config on these wiki pages:
|
||||
|
||||
* [`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)
|
||||
* [`layer-rule {}`](./Configuration:-Layer-Rules.md)
|
||||
* [`animations {}`](./Configuration:-Animations.md)
|
||||
* [`gestures {}`](./Configuration:-Gestures.md)
|
||||
* [`debug {}`](./Configuration:-Debug-Options.md)
|
||||
|
||||
### Loading
|
||||
|
||||
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
Please use the default configuration file as the starting point for your custom configuration.
|
||||
|
||||
The configuration is live-reloaded.
|
||||
Simply edit and save the config file, and your changes will be applied.
|
||||
This includes key bindings, output settings like mode, window rules, and everything else.
|
||||
|
||||
You can run `niri validate` to parse the config and see any errors.
|
||||
|
||||
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
|
||||
|
||||
You can also set `$NIRI_CONFIG` to the path of the config file.
|
||||
`--config` always takes precedence.
|
||||
If `--config` or `$NIRI_CONFIG` doesn't point to a real file, the config will not be loaded.
|
||||
If `$NIRI_CONFIG` is set to an empty string, it is ignored and the default config location is used instead.
|
||||
|
||||
### Syntax
|
||||
|
||||
The config is written in [KDL].
|
||||
|
||||
#### Comments
|
||||
|
||||
Lines starting with `//` are comments; they are ignored.
|
||||
|
||||
Also, you can put `/-` in front of a section to comment out the entire section:
|
||||
|
||||
```kdl
|
||||
/-output "eDP-1" {
|
||||
// Everything inside here is ignored.
|
||||
// The display won't be turned off
|
||||
// as the whole section is commented out.
|
||||
off
|
||||
}
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
Toggle options in niri are commonly represented as flags.
|
||||
Writing out the flag enables it, and omitting it or commenting it out disables it.
|
||||
For example:
|
||||
|
||||
```kdl
|
||||
// "Focus follows mouse" is enabled.
|
||||
input {
|
||||
focus-follows-mouse
|
||||
|
||||
// Other settings...
|
||||
}
|
||||
```
|
||||
|
||||
```kdl
|
||||
// "Focus follows mouse" is disabled.
|
||||
input {
|
||||
// focus-follows-mouse
|
||||
|
||||
// Other settings...
|
||||
}
|
||||
```
|
||||
|
||||
#### Sections
|
||||
|
||||
Most sections cannot be repeated. For example:
|
||||
|
||||
```kdl
|
||||
// This is valid: every section appears once.
|
||||
input {
|
||||
keyboard {
|
||||
// ...
|
||||
}
|
||||
|
||||
touchpad {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kdl,must-fail
|
||||
// This is NOT valid: input section appears twice.
|
||||
input {
|
||||
keyboard {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touchpad {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exceptions are, for example, sections that configure different devices by name:
|
||||
|
||||
<!-- NOTE: this may break in the future -->
|
||||
```kdl
|
||||
output "eDP-1" {
|
||||
// ...
|
||||
}
|
||||
|
||||
// This is valid: this section configures a different output.
|
||||
output "HDMI-A-1" {
|
||||
// ...
|
||||
}
|
||||
|
||||
// This is NOT valid: "eDP-1" already appeared above.
|
||||
// It will either throw a config parsing error, or otherwise not work.
|
||||
output "eDP-1" {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Defaults
|
||||
|
||||
Omitting most of the sections of the config file will leave you with the default values for that section.
|
||||
A notable exception is [`binds {}`](./Configuration:-Key-Bindings.md): they do not get filled with defaults, so make sure you do not erase this section.
|
||||
|
||||
### Breaking Change Policy
|
||||
|
||||
As a rule, niri updates should not break existing config files.
|
||||
(For example, the default config from niri v0.1.0 still parses fine on v25.02 as I'm writing this.)
|
||||
|
||||
Exceptions can be made for parsing bugs.
|
||||
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
|
||||
A patch release changed niri from silently accepting this to causing a parsing failure.
|
||||
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
|
||||
|
||||
Keep in mind that the breaking change policy applies only to niri releases.
|
||||
Commits between releases can and do occasionally break the config as new features are ironed out.
|
||||
However, I do try to limit these, since several people are running git builds.
|
||||
|
||||
[KDL]: https://kdl.dev/
|
||||
This wiki page has moved to: [Introduction](./Configuration:-Introduction.md).
|
||||
|
||||
@@ -35,6 +35,7 @@ window-rule {
|
||||
match is-active-in-column=true
|
||||
match is-floating=true
|
||||
match is-window-cast-target=true
|
||||
match is-urgent=true
|
||||
match at-startup=true
|
||||
|
||||
// Properties that apply once upon window opening.
|
||||
@@ -63,8 +64,10 @@ window-rule {
|
||||
width 4
|
||||
active-color "#7fc8ff"
|
||||
inactive-color "#505050"
|
||||
urgent-color "#9b0000"
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
// urgent-gradient from="#800" to="#a33" angle=45
|
||||
}
|
||||
|
||||
border {
|
||||
@@ -85,8 +88,10 @@ window-rule {
|
||||
tab-indicator {
|
||||
active-color "red"
|
||||
inactive-color "gray"
|
||||
urgent-color "blue"
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
// urgent-gradient from="#800" to="#a33" angle=45
|
||||
}
|
||||
|
||||
geometry-corner-radius 12
|
||||
@@ -282,6 +287,19 @@ Example:
|
||||
|
||||

|
||||
|
||||
#### `is-urgent`
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Can be `true` or `false`.
|
||||
Matches windows that request the user's attention.
|
||||
|
||||
```kdl
|
||||
window-rule {
|
||||
match is-urgent=true
|
||||
}
|
||||
```
|
||||
|
||||
#### `at-startup`
|
||||
|
||||
<sup>Since: 0.1.6</sup>
|
||||
@@ -829,7 +847,7 @@ window-rule {
|
||||
|
||||
#### `tiled-state`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Informs the window that it is tiled.
|
||||
Usually, windows will react by becoming rectangular and hiding their client-side shadows.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ Then niri will ask windows to omit client-side decorations, and also inform them
|
||||
Note that currently this will prevent edge window resize handles from showing up.
|
||||
You can still resize windows by holding <kbd>Mod</kbd> and the right mouse button.
|
||||
|
||||
### Why is the border/focus ring showing up through semitransparent windows?
|
||||
### Why are transparent windows tinted? / Why is the border/focus ring showing up through semitransparent windows?
|
||||
|
||||
Uncomment the [`prefer-no-csd` setting](./Configuration:-Miscellaneous.md#prefer-no-csd) at the top level of the config, and then restart your apps.
|
||||
Niri will draw focus rings and borders *around* windows that agree to omit their client-side decorations.
|
||||
|
||||
@@ -68,3 +68,26 @@ Move the view horizontally with three-finger horizontal swipes.
|
||||
|
||||
Scroll the tiling view when moving the mouse cursor against a monitor edge during drag-and-drop (DnD).
|
||||
Also works on a touchscreen.
|
||||
|
||||
#### Drag-and-Drop Edge Workspace Switch
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Scroll the workspaces up/down when moving the mouse cursor against a monitor edge during drag-and-drop (DnD) while in the overview.
|
||||
Also works on a touchscreen.
|
||||
|
||||
#### Drag-and-Drop Hold to Activate
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
While drag-and-dropping, hold your mouse over a window to activate it.
|
||||
This will bring a floating window to the top for example.
|
||||
|
||||
In the overview, you can also hold the mouse over a workspace to switch to it.
|
||||
|
||||
#### Hot Corner to Toggle the Overview
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Put your mouse at the very top-left corner of a monitor to toggle the overview.
|
||||
Also works during drag-and-dropping something.
|
||||
|
||||
@@ -17,7 +17,7 @@ Then it will open as a window, where you can give it a try.
|
||||
Note that this windowed mode is mainly meant for development, so it is a bit buggy (in particular, there are issues with hotkeys).
|
||||
|
||||
Next, see the [list of important software](./Important-Software.md) required for normal desktop use, like a notification daemon and portals.
|
||||
Also, check the [configuration overview](./Configuration:-Overview.md) page to get started configuring niri.
|
||||
Also, check the [configuration introduction](./Configuration:-Introduction.md) page to get started configuring niri.
|
||||
There you can find links to other pages containing thorough documentation and examples for all options.
|
||||
Finally, the [Xwayland](./Xwayland.md) page explains how to run X11 applications on niri.
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ Things to keep in mind with layer-shell components (bars, launchers, etc.):
|
||||
|
||||
1. When a full-screen window is active and covers the entire screen, it will render above the top layer, and it will be prioritized for keyboard focus. If your launcher uses the top layer, and you try to run it while looking at a full-screen window, it won't show up. Only the overlay layer will show up on top of full-screen windows.
|
||||
1. Components on the bottom and background layers will receive *on-demand* keyboard focus as expected. However, they will only receive *exclusive* keyboard focus when there are no windows on the workspace.
|
||||
1. When opening the [Overview](./Overview.md), components on the bottom and background layers will zoom out and remain on the workspaces, while the top and overlay layers remain on top of the Overview. So, if you want the bar to remain on top, put it on the *top* layer.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
### Overview
|
||||
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
The Overview is a zoomed-out view of your workspaces and windows.
|
||||
It lets you see what's going on at a glance, navigate, and drag windows around.
|
||||
|
||||
https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995
|
||||
|
||||
Open it with the `toggle-overview` bind, via the top-left hot corner, or using a touchpad four-finger swipe up.
|
||||
While in the overview, all keyboard shortcuts keep working, while pointing devices get easier:
|
||||
|
||||
- Mouse: left click and drag windows to move them, right click and drag to scroll workspaces left/right, scroll to switch workspaces (no holding Mod required).
|
||||
- Touchpad: two-finger scrolling that matches the normal three-finger gestures.
|
||||
- Touchscreen: one-finger scrolling, or one-finger long press to move a window.
|
||||
|
||||
> [!TIP]
|
||||
> The overview needs to draw a background under every workspace.
|
||||
> So, layer-shell surfaces work this way: the *background* and *bottom* layers zoom out together with the workspaces, while the *top* and *overlay* layers remain on top of the overview.
|
||||
>
|
||||
> Put your bar on the *top* layer.
|
||||
|
||||
Drag-and-drop will scroll the workspaces up/down in the overview, and will activate a workspace when holding it for a moment.
|
||||
Combined with the hot corner, this lets you do a mouse-only DnD across workspaces.
|
||||
|
||||
https://github.com/user-attachments/assets/5f09c5b7-ff40-462b-8b9c-f1b8073a2cbb
|
||||
|
||||
You can also drag-and-drop a window to a new workspace above, below, or between existing workspaces.
|
||||
|
||||
https://github.com/user-attachments/assets/b76d5349-aa20-4889-ab90-0a51554c789d
|
||||
|
||||
### Configuration
|
||||
|
||||
See the full documentation for the `overview {}` section [here](./Configuration:-Miscellaneous.md#overview).
|
||||
|
||||
You can set the zoom-out level like this:
|
||||
|
||||
```kdl
|
||||
// Make workspaces four times smaller than normal in the overview.
|
||||
overview {
|
||||
zoom 0.25
|
||||
}
|
||||
```
|
||||
|
||||
To change the color behind the workspaces, use the `backdrop-color` setting:
|
||||
|
||||
```kdl
|
||||
// Make the backdrop light.
|
||||
overview {
|
||||
backdrop-color "#777777"
|
||||
}
|
||||
```
|
||||
|
||||
You can also disable the hot corner:
|
||||
|
||||
```kdl
|
||||
// Disable the hot corners.
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backdrop customization
|
||||
|
||||
Apart from setting a custom backdrop color like described above, you can also put a layer-shell wallpaper into the backdrop with a [layer rule](./Configuration:-Layer-Rules.md#place-within-backdrop), for example:
|
||||
|
||||
```kdl
|
||||
// Put swaybg inside the overview backdrop.
|
||||
layer-rule {
|
||||
match namespace="^wallpaper$"
|
||||
place-within-backdrop true
|
||||
}
|
||||
```
|
||||
|
||||
This will only work for *background* layer surfaces that ignore exclusive zones (typical for wallpaper tools).
|
||||
|
||||
You can run two different wallpaper tools (like swaybg and swww), one for the backdrop and one for the normal workspace background.
|
||||
This way you could set the backdrop one to a blurred version of the wallpaper for a nice effect.
|
||||
|
||||
You can also combine this with a transparent background color if you don't like the wallpaper moving together with workspaces:
|
||||
|
||||
```kdl
|
||||
// Make the wallpaper stationary, rather than moving with workspaces.
|
||||
layer-rule {
|
||||
// This is for swaybg; change for other wallpaper tools.
|
||||
// Find the right namespace by running niri msg layers.
|
||||
match namespace="^wallpaper$"
|
||||
place-within-backdrop true
|
||||
}
|
||||
|
||||
// Set transparent workspace background color.
|
||||
layout {
|
||||
background-color "transparent"
|
||||
}
|
||||
|
||||
// Optionally, disable the workspace shadows in the overview.
|
||||
overview {
|
||||
workspace-shadow {
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -46,7 +46,7 @@ Check [the corresponding wiki section](./Configuration:-Window-Rules.md#block-ou
|
||||
|
||||
### Dynamic screencast target
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
Niri provides a special screencast stream that you can change dynamically.
|
||||
It shows up as "niri Dynamic Cast Target" in the screencast window dialog.
|
||||
@@ -113,7 +113,7 @@ Example:
|
||||
|
||||
### Windowed (fake/detached) fullscreen
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 25.05</sup>
|
||||
|
||||
When screencasting browser-based presentations like Google Slides, you usually want to hide the browser UI, which requires making the browser fullscreen.
|
||||
This is not always convenient, for example if you have an ultrawide monitor, or just want to leave the browser as a smaller window, without taking up an entire monitor.
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
* [Workspaces](./Workspaces.md)
|
||||
* [Floating Windows](./Floating-Windows.md)
|
||||
* [Tabs](./Tabs.md)
|
||||
* [Overview](./Overview.md)
|
||||
* [Screencasting](./Screencasting.md)
|
||||
* [Layer‐Shell Components](./Layer%E2%80%90Shell-Components.md)
|
||||
* [IPC, `niri msg`](./IPC.md)
|
||||
@@ -15,7 +16,7 @@
|
||||
* [FAQ](./FAQ.md)
|
||||
|
||||
## Configuration
|
||||
* [Overview](./Configuration:-Overview.md)
|
||||
* [Introduction](./Configuration:-Introduction.md)
|
||||
* [Input](./Configuration:-Input.md)
|
||||
* [Outputs](./Configuration:-Outputs.md)
|
||||
* [Key Bindings](./Configuration:-Key-Bindings.md)
|
||||
|
||||
Reference in New Issue
Block a user