Compare commits

...

18 Commits

Author SHA1 Message Date
dependabot[bot] 66d6355b25 build(deps): bump the rust-dependencies group across 1 directory with 3 updates
Bumps the rust-dependencies group with 3 updates in the / directory: [clap_complete](https://github.com/clap-rs/clap), [libc](https://github.com/rust-lang/libc) and [zbus](https://github.com/z-galaxy/zbus).


Updates `clap_complete` from 4.6.2 to 4.6.3
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.2...clap_complete-v4.6.3)

Updates `libc` from 0.2.185 to 0.2.186
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.186/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.185...0.2.186)

Updates `zbus` from 5.13.2 to 5.15.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus-5.13.2...zbus-5.15.0)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.186
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zbus
  dependency-version: 5.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 08:46:50 +00:00
ArtikusHG 56654034e9 Prevent leaving an orphaned shell process when using niri-session 2026-05-05 12:30:34 -07:00
Ivan Molodetskikh 1f07cffa9f Allow spawn and spawn-sh while in the screenshot UI 2026-05-02 17:55:45 +03:00
Ivan Molodetskikh cb3a06cd54 Move existing screenshot selection when holding Mod 2026-05-02 14:14:09 +03:00
Ivan Molodetskikh f115f2e5e7 Clamp pointer to output inside screenshot_ui
- Removes some duplication
- Allows for better handling by screenshot UI itself depending on the
  case
2026-05-02 13:32:31 +03:00
Ivan Molodetskikh 2e07282977 input: Extract mod_down upwards
View diff with whitespace ignored.
2026-05-02 13:31:13 +03:00
Ivan Molodetskikh cba0454c94 input: Filter allowed_during_screenshot() for non-kb binds
Forgot about it for those.
2026-05-02 12:06:52 +03:00
Ivan Molodetskikh 5f6f131b24 Add map-to-focused-window tablet setting 2026-05-01 12:18:00 +03:00
Ivan Molodetskikh adb5b3cd2c input/tablet: Use f64 for the target rectangle 2026-05-01 12:08:33 +03:00
Ivan Molodetskikh 0650e7b640 layout: Change active_tile_visual_rectangle() to window
New functionality needs window specifically, and existing logic will be
fine with window too.
2026-04-30 19:20:50 +03:00
Canmi dd1c3bcb9f fix: link to hyprland scrolling layout docs 2026-04-29 06:10:55 -07:00
Dagmawi Ali e5d463e15b render: fix blur on OpenGL ES 2.0 GPUs 2026-04-29 02:43:46 -07:00
Dimitry Ishenko 26100096e8 Revert "Stop including broken LFS files in source tarball"
This reverts commit d8265ad34e.
2026-04-27 09:03:06 -07:00
Ivan Molodetskikh a85b922919 wiki/security: Add a lock screen section 2026-04-27 00:11:59 +03:00
Ivan Molodetskikh 7d2b620ce9 wiki/security: Mention X11 2026-04-27 00:11:59 +03:00
Nick Janetakis a48f2645d9 Add additional video reference 2026-04-26 07:01:18 -07:00
Ivan Molodetskikh 83e839762f wiki: Document the security model 2026-04-26 15:03:14 +03:00
Ivan Molodetskikh 4c1196f45b wiki/releasing: Add updating wayland.app step 2026-04-26 10:46:13 +03:00
20 changed files with 428 additions and 275 deletions
-10
View File
@@ -1,12 +1,2 @@
# LFS configuration for images from the wiki
*.png filter=lfs diff=lfs merge=lfs -text
# Exclude LFS-tracked files from the tarball
/docs/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'
Generated
+23 -32
View File
@@ -714,9 +714,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.6.2"
version = "4.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3"
dependencies = [
"clap",
]
@@ -1963,9 +1963,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libdisplay-info"
@@ -3764,7 +3764,7 @@ dependencies = [
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow 1.0.1",
"winnow",
]
[[package]]
@@ -3785,7 +3785,7 @@ dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow 1.0.1",
"winnow",
]
[[package]]
@@ -3794,7 +3794,7 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow 1.0.1",
"winnow",
]
[[package]]
@@ -4556,15 +4556,6 @@ dependencies = [
"xkbcommon-dl",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.1"
@@ -4765,9 +4756,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zbus"
version = "5.13.2"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1"
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
dependencies = [
"async-broadcast",
"async-executor",
@@ -4792,7 +4783,7 @@ dependencies = [
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 0.7.15",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
@@ -4824,9 +4815,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.13.2"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1"
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -4839,12 +4830,12 @@ dependencies = [
[[package]]
name = "zbus_names"
version = "4.3.1"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow 0.7.15",
"winnow",
"zvariant",
]
@@ -4888,23 +4879,23 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zvariant"
version = "5.9.2"
version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4"
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 0.7.15",
"winnow",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.9.2"
version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c"
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -4915,13 +4906,13 @@ dependencies = [
[[package]]
name = "zvariant_utils"
version = "3.3.0"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"winnow 0.7.15",
"winnow",
]
+5 -2
View File
@@ -48,7 +48,10 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
Also check out these videos that showcase a lot of the niri functionality:
- [Niri Is My New Favorite Wayland Compositor](https://www.youtube.com/watch?v=DeYx2exm04M) by Brodie Robertson
- [How Is niri This Good? Live Demo + Config](https://www.youtube.com/watch?v=7XmD5UyyhZQ) by Nick Janetakis
## Status
@@ -110,7 +113,7 @@ 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.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Scrolling-Layout/).
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/).
- [Paneru] and [PaperWM.spoon]: scrollable tiling on top of macOS.
## Contact
+1
View File
@@ -88,6 +88,7 @@ nav:
- Window Effects: Window-Effects.md
- Packaging niri: Packaging-niri.md
- Integrating niri: Integrating-niri.md
- Security Model: Security-Model.md
- Accessibility: Accessibility.md
- Name and Logo: Name-and-Logo.md
- FAQ: FAQ.md
+8 -1
View File
@@ -90,6 +90,7 @@ input {
// off
map-to-output "eDP-1"
// map-to-focused-output
// map-to-focused-window
// left-handed
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
}
@@ -282,10 +283,16 @@ Valid output names are the same as the ones used for output configuration.
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
Setting specific to `tablet`:
Settings specific to `tablet`:
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
- `map-to-focused-window`: <sup>Since: next release</sup> will map the tablet to the focused window's geometry, takes precedence over `map-to-focused-output` and `map-to-output`.
Falls back to those when no window is focused (for example, in the overview).
When the tablet is also mapped to a specific output via `map-to-output`, the `map-to-focused-window` flag will map the tablet to the active window on that output.
If the tablet isn't mapped to any specific output, it will map the tablet to the current focused window regardless of where it is.
### General Settings
These settings are not specific to a particular input device.
+16
View File
@@ -128,3 +128,19 @@ copr-cli build niri niri.spec
## Announce the release
Chat rooms, social media, etc.
## Update wayland.app protocol data
- Install [wlprobe](https://github.com/PolyMeilex/wlprobe).
- Clone https://github.com/vially/wayland-explorer.
- Generate data:
```
wlprobe > ./src/data/compositors/niri.json
```
- Manually add `"version": "26.04"`, then clean up the diff from unrelated changes, for example:
- The number of `wl_output`s will change depending on how many monitors you have connected.
- The number of `wp_drm_lease_device_v1` will change depending on your number of GPUs.
- `org_kde_kwin_server_decoration_manager` and `zxdg_decoration_manager_v1` will only appear with `prefer-no-csd`.
- Create a pull request.
+4
View File
@@ -63,3 +63,7 @@ Alternatively, some desktop environments and shells work with niri, and can give
- Many [XFCE](https://www.xfce.org/) components work on Wayland, including niri. See [their wiki](https://wiki.xfce.org/releng/wayland_roadmap#component_specific_status) for details.
- There are complete desktop shells based on Quickshell that support niri, for example [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell) and [Noctalia](https://github.com/noctalia-dev/noctalia-shell).
- You can run a [COSMIC](https://system76.com/cosmic/) session with niri using [cosmic-ext-extra-sessions](https://github.com/Drakulix/cosmic-ext-extra-sessions).
### Security model
See the [Security Model](./Security-Model.md) page for an overview of niri's security model.
+61
View File
@@ -0,0 +1,61 @@
Niri assumes that programs running unsandboxed on the host are **trusted**.
This is a reasonable assumption because programs running on the host have a wide variety of ways to get all access they need, even without niri.
For instance:
- They can set `$LD_PRELOAD` in `.bashrc` or similar files to load an arbitrary library into all processes.
- They can replace binaries in `$PATH` with malicious code.
- They can interpose any socket in `$XDG_RUNTIME_DIR`, like Wayland, and do keylogging or record window contents.
- They can scan the filesystem for secrets: SSH keys, password stores, etc.
- They can connect to an unlocked keyring and steal credentials.
- And so on and so forth.
## Unsandboxed clients
Anything with access to niri's Wayland socket can, among other things:
- Record the user's screen via [wlr-screencopy](https://wayland.app/protocols/wlr-screencopy-unstable-v1).
- Emulate input via [wlr-virtual-pointer](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard](https://wayland.app/protocols/virtual-keyboard-unstable-v1).
- Get the user's clipboard contents via [wlr-data-control](https://wayland.app/protocols/ext-data-control-v1).
- Create arbitrary fullscreen surfaces through [wlr-layer-shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) that can steal the user's input, pretend to be a password entry, or lock the user out of their session.
- Kill a running lockscreen, create a new lock surface, and tell niri to unlock a locked session.
Anything with access to niri's [IPC](./IPC.md) socket can, among other things:
- Spawn a Wayland client which can do everything in the list above.
Anything with access to niri's D-Bus interfaces can, among other things:
- Record the user's screen via the screencast interface.
- Fully listen to and emulate input from the user's keyboard via the accessibility interface.
Also, while niri doesn't directly integrate Xwayland, it's worth reminding that anything with access to the X11 `$DISPLAY` (which comes both as a socket file on disk **and** as an abstract socket in the network namespace) can intercept and emulate all input and record the contents of any X11 windows on the same `$DISPLAY` (but not Wayland windows).
## Running untrusted clients
Considering all of the above, for running untrusted clients, you need a proper sandbox that:
- Removes niri's IPC socket.
- Prevents D-Bus access to host services.
- Uses a filtered Wayland socket.
For creating a filtered Wayland socket, you can use the [security-context](https://wayland.app/protocols/security-context-v1) protocol which niri implements.
All unsafe protocols are made inaccessible through this filtered Wayland socket.
One sandbox that satisfies all of these criteria is the [Flatpak](https://flatpak.org/) sandbox.
Importantly, filtering just the Wayland socket (and leaving, for example, unrestricted D-Bus access) is **not enough** to prevent untrusted clients from doing bad things.
## Lock screen
When the session is locked via [ext-session-lock](https://wayland.app/protocols/ext-session-lock-v1), most actions (keybindings) are automatically disabled.
Only a very small set of safe actions is allowed.
In particular, spawning will not work, with the exception of binds explicitly configured with `allow-when-locked=true`.
Importantly, the **quit** action is allowed—you can always quit niri, even when on a lock screen.
Therefore, you must ensure that quitting niri does not drop you into an unprotected TTY commandline.
Usually, a display manager, like GDM, will do this for you: when niri exits (via the quit bind or if it crashes), it'll put you back into a safe password prompt.
Other than quitting, the only way to exit a lock screen is for the lock screen client to tell niri to unlock the session.
If the lock screen client crashes, the session remains locked with a solid red background.
In this case, another lock screen client can take over (so you can start a fresh lock screen if it crashes, and still unlock your session).
+1
View File
@@ -17,6 +17,7 @@
* [Window Effects](./Window-Effects.md)
* [Packaging niri](./Packaging-niri.md)
* [Integrating niri](./Integrating-niri.md)
* [Security Model](./Security-Model.md)
* [Accessibility](./Accessibility.md)
* [Name and Logo](./Name-and-Logo.md)
* [FAQ](./FAQ.md)
+2
View File
@@ -366,6 +366,8 @@ pub struct Tablet {
#[knuffel(child)]
pub map_to_focused_output: bool,
#[knuffel(child)]
pub map_to_focused_window: bool,
#[knuffel(child)]
pub left_handed: bool,
}
+2
View File
@@ -720,6 +720,7 @@ mod tests {
tablet {
map-to-output "eDP-1"
map-to-focused-output
map-to-focused-window
calibration-matrix 1.0 2.0 3.0 \
4.0 5.0 6.0
}
@@ -1113,6 +1114,7 @@ mod tests {
"eDP-1",
),
map_to_focused_output: true,
map_to_focused_window: true,
left_handed: false,
},
touch: Touch {
+1 -1
View File
@@ -15,7 +15,7 @@ if [ -n "$SHELL" ] &&
! (echo "$SHELL" | grep -q "false") &&
! (echo "$SHELL" | grep -q "nologin"); then
if [ "$1" != '-l' ]; then
exec bash -c "exec -l '$SHELL' -c '$0 -l $*'"
exec bash -c "exec -l '$SHELL' -c 'exec $0 -l $*'"
else
shift
fi
+247 -198
View File
@@ -1,5 +1,4 @@
use std::any::Any;
use std::cmp::min;
use std::collections::hash_map::Entry;
use std::collections::HashSet;
use std::time::Duration;
@@ -297,41 +296,68 @@ impl State {
let device_output = event.device().output(self);
let device_output = device_output.filter(|output| self.niri.output_exists(output));
let device_output = device_output.as_ref();
let (target_geo, keep_ratio, px, transform) =
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
(
self.niri.global_space.output_geometry(output).unwrap(),
true,
1. / output.current_scale().fractional_scale(),
output.current_transform(),
)
} else {
let geo = self.global_bounding_rectangle()?;
let mapped_output = device_output.or_else(|| self.niri.output_for_tablet());
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
// corresponding to the position on the right when clamping.
let output = self.niri.global_space.outputs().next().unwrap();
let scale = output.current_scale().fractional_scale();
// If the tablet is configured to map to the focused window, use that window's geometry on
// the mapped output (or on the focused output if no specific output is mapped).
let map_to_focused_window = self.niri.config.borrow().input.tablet.map_to_focused_window;
// But only if the keyboard focus is on the layout, so that it doesn't trigger on the lock
// screen and such.
let window_target = if map_to_focused_window && self.niri.keyboard_focus.is_layout() {
let output = mapped_output.or_else(|| self.niri.layout.active_output());
output.and_then(|output| {
let monitor = self.niri.layout.monitor_for_output(output)?;
let mut rect = monitor.active_window_visual_rectangle()?;
let output_geo = self.niri.global_space.output_geometry(output)?;
rect.loc += output_geo.loc.to_f64();
Some((rect, output))
})
} else {
None
};
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
(geo, false, 1. / scale, Transform::Normal)
};
let (target_geo, keep_ratio, px, transform) = if let Some((rect, output)) = window_target {
(
rect,
true,
1. / output.current_scale().fractional_scale(),
output.current_transform(),
)
} else if let Some(output) = mapped_output {
let geo = self.niri.global_space.output_geometry(output).unwrap();
(
geo.to_f64(),
true,
1. / output.current_scale().fractional_scale(),
output.current_transform(),
)
} else {
let geo = self.global_bounding_rectangle()?.to_f64();
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
// corresponding to the position on the right when clamping.
let output = self.niri.global_space.outputs().next().unwrap();
let scale = output.current_scale().fractional_scale();
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
(geo, false, 1. / scale, Transform::Normal)
};
let mut pos = {
let size = transform.invert().transform_size(target_geo.size);
transform.transform_point_in(event.position_transformed(size), &size.to_f64())
transform.transform_point_in(event.position_transformed(size.to_i32_round()), &size)
};
if keep_ratio {
pos.x /= target_geo.size.w as f64;
pos.y /= target_geo.size.h as f64;
pos.x /= target_geo.size.w;
pos.y /= target_geo.size.h;
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if let Some(data) = self.niri.tablets.get(device) {
// This code does the same thing as mutter with "keep aspect ratio" enabled.
let size = transform.invert().transform_size(target_geo.size);
let output_aspect_ratio = size.w as f64 / size.h as f64;
let output_aspect_ratio = size.w / size.h;
let ratio = data.aspect_ratio / output_aspect_ratio;
if ratio > 1. {
@@ -342,13 +368,13 @@ impl State {
}
};
pos.x *= target_geo.size.w as f64;
pos.y *= target_geo.size.h as f64;
pos.x *= target_geo.size.w;
pos.y *= target_geo.size.h;
}
pos.x = pos.x.clamp(0.0, target_geo.size.w as f64 - px);
pos.y = pos.y.clamp(0.0, target_geo.size.h as f64 - px);
Some(pos + target_geo.loc.to_f64())
pos.x = pos.x.clamp(0.0, target_geo.size.w - px);
pos.y = pos.y.clamp(0.0, target_geo.size.h - px);
Some(pos + target_geo.loc)
}
fn is_inhibiting_shortcuts(&self) -> bool {
@@ -2520,16 +2546,10 @@ impl State {
if let Some(output) = self.niri.screenshot_ui.selection_output() {
let geom = self.niri.global_space.output_geometry(output).unwrap();
let mut point = (new_pos - geom.loc.to_f64())
let point = (new_pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round::<i32>();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = point.x.clamp(0, size.w - 1);
point.y = point.y.clamp(0, size.h - 1);
self.niri.screenshot_ui.pointer_motion(point, None);
}
@@ -2657,16 +2677,10 @@ impl State {
if let Some(output) = self.niri.screenshot_ui.selection_output() {
let geom = self.niri.global_space.output_geometry(output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round::<i32>();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = point.x.clamp(0, size.w - 1);
point.y = point.y.clamp(0, size.h - 1);
self.niri.screenshot_ui.pointer_motion(point, None);
}
@@ -2751,10 +2765,11 @@ impl State {
return;
}
if ButtonState::Pressed == button_state {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
let mod_down = modifiers.contains(mod_key.to_modifiers());
if ButtonState::Pressed == button_state {
let mut is_mru_open = false;
if let Some(mru_output) = self.niri.window_mru_ui.output() {
is_mru_open = true;
@@ -2791,6 +2806,9 @@ impl State {
let bindings =
make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers);
find_configured_bind(bindings, mod_key, trigger, mods)
})
.filter(|bind| {
!self.niri.screenshot_ui.is_open() || allowed_during_screenshot(&bind.action)
}) {
self.niri.suppressed_buttons.insert(button_code);
self.handle_bind(bind.clone());
@@ -2832,44 +2850,41 @@ impl State {
}
}
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
// We don't want to accidentally "catch" the wrong workspace during
// animations.
self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
})
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() && mod_down {
let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
// We don't want to accidentally "catch" the wrong workspace during
// animations.
self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
})
};
if let Some((output, ws)) = output_ws {
let ws_id = ws.id();
self.niri.layout.focus_output(&output);
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
if let Some((output, ws)) = output_ws {
let ws_id = ws.id();
// FIXME: granular.
self.niri.queue_redraw_all();
self.niri.layout.focus_output(&output);
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
// FIXME: granular.
self.niri.queue_redraw_all();
// Don't activate the window under the cursor to avoid unnecessary
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
return;
}
// Don't activate the window under the cursor to avoid unnecessary
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
return;
}
}
@@ -2878,7 +2893,6 @@ impl State {
// Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if is_overview_open || mod_down {
let location = pointer.current_location();
@@ -2912,72 +2926,69 @@ impl State {
}
}
// Check if we need to start an interactive resize.
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let edges = self
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() && mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let edges = self
.niri
.layout
.resize_edges_under(output, pos_within_output)
.unwrap_or(ResizeEdge::empty());
if !edges.is_empty() {
// See if we got a double resize-click gesture.
// FIXME: deduplicate with resize_request in xdg-shell somehow.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let mut last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click
// gestures, so just allow it to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
last_cell.set(None);
let intersection = edges.intersection(last_edges);
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
// FIXME: don't activate once we can pass specific windows
// to actions.
self.niri.layout.activate_window(&window);
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
self.niri.layout.activate_window(&window);
self.niri.layout.reset_window_height(Some(&window));
}
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
self.niri.layout.activate_window(&window);
if self
.niri
.layout
.resize_edges_under(output, pos_within_output)
.unwrap_or(ResizeEdge::empty());
if !edges.is_empty() {
// See if we got a double resize-click gesture.
// FIXME: deduplicate with resize_request in xdg-shell somehow.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let mut last = last_cell.get();
last_cell.set(Some((time, edges)));
// Floating windows don't have either of the double-resize-click
// gestures, so just allow it to resize.
if mapped.is_floating() {
last = None;
last_cell.set(None);
}
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
last_cell.set(None);
let intersection = edges.intersection(last_edges);
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
// FIXME: don't activate once we can pass specific windows
// to actions.
self.niri.layout.activate_window(&window);
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
self.niri.layout.activate_window(&window);
self.niri.layout.reset_window_height(Some(&window));
}
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
self.niri.layout.activate_window(&window);
if self
.niri
.layout
.interactive_resize_begin(window.clone(), edges)
{
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
let grab = ResizeGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.cursor_manager.set_cursor_image(
CursorImageStatus::Named(edges.cursor_icon()),
);
}
.interactive_resize_begin(window.clone(), edges)
{
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
let grab = ResizeGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(edges.cursor_icon()));
}
}
}
@@ -3017,20 +3028,25 @@ impl State {
if button == Some(MouseButton::Left) && self.niri.screenshot_ui.is_open() {
if button_state == ButtonState::Pressed {
let pos = pointer.current_location();
if let Some((output, _)) = self.niri.output_under(pos) {
let output = output.clone();
// If we'll be moving the existing selection, use the selection output.
let output = if mod_down {
self.niri.screenshot_ui.selection_output()
} else {
self.niri.output_under(pos).map(|(out, _)| out)
};
if let Some(output) = output.cloned() {
let geom = self.niri.global_space.output_geometry(&output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = min(size.w - 1, point.x);
point.y = min(size.h - 1, point.y);
if self.niri.screenshot_ui.pointer_down(output, point, None) {
if self
.niri
.screenshot_ui
.pointer_down(output, point, None, mod_down)
{
self.niri.queue_redraw_all();
}
}
@@ -3146,13 +3162,21 @@ impl State {
mod_key,
Trigger::WheelScrollLeft,
mods,
);
)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
let bind_right = find_configured_bind(
bindings,
mod_key,
Trigger::WheelScrollRight,
mods,
);
)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
(bind_left, bind_right)
};
@@ -3233,9 +3257,17 @@ impl State {
mod_key,
Trigger::WheelScrollUp,
mods,
);
)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
let bind_down =
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods);
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
(bind_up, bind_down)
};
@@ -3378,9 +3410,17 @@ impl State {
mod_key,
Trigger::TouchpadScrollLeft,
mods,
);
)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
let bind_right =
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods);
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
drop(config);
if let Some(right) = bind_right {
@@ -3408,9 +3448,17 @@ impl State {
mod_key,
Trigger::TouchpadScrollUp,
mods,
);
)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
let bind_down =
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods);
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods)
.filter(|bind| {
!self.niri.screenshot_ui.is_open()
|| allowed_during_screenshot(&bind.action)
});
drop(config);
if let Some(down) = bind_down {
@@ -3514,16 +3562,10 @@ impl State {
if let Some(output) = self.niri.screenshot_ui.selection_output() {
let geom = self.niri.global_space.output_geometry(output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round::<i32>();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = point.x.clamp(0, size.w - 1);
point.y = point.y.clamp(0, size.h - 1);
self.niri.screenshot_ui.pointer_motion(point, None);
}
@@ -3596,19 +3638,29 @@ impl State {
let under = self.niri.contents_under(pos);
if self.niri.screenshot_ui.is_open() {
if let Some(output) = under.output.clone() {
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
let mod_down = modifiers.contains(mod_key.to_modifiers());
// If we'll be moving the existing selection, use the selection output.
let output = if mod_down {
self.niri.screenshot_ui.selection_output()
} else {
under.output.as_ref()
};
if let Some(output) = output.cloned() {
let geom = self.niri.global_space.output_geometry(&output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = min(size.w - 1, point.x);
point.y = min(size.h - 1, point.y);
if self.niri.screenshot_ui.pointer_down(output, point, None) {
if self
.niri
.screenshot_ui
.pointer_down(output, point, None, mod_down)
{
self.niri.queue_redraw_all();
}
}
@@ -4070,24 +4122,28 @@ impl State {
let under = self.niri.contents_under(pos);
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mods = modifiers_from_state(mods);
let mod_down = mods.contains(mod_key.to_modifiers());
if self.niri.screenshot_ui.is_open() {
if let Some(output) = under.output.clone() {
// If we'll be moving the existing selection, use the selection output.
let output = if mod_down {
self.niri.screenshot_ui.selection_output()
} else {
under.output.as_ref()
};
if let Some(output) = output.cloned() {
let geom = self.niri.global_space.output_geometry(&output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = min(size.w - 1, point.x);
point.y = min(size.h - 1, point.y);
if self
.niri
.screenshot_ui
.pointer_down(output, point, Some(slot))
.pointer_down(output, point, Some(slot), mod_down)
{
self.niri.queue_redraw_all();
}
@@ -4106,10 +4162,6 @@ impl State {
}
}
} else if !handle.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mods = modifiers_from_state(mods);
let mod_down = mods.contains(mod_key.to_modifiers());
if self.niri.layout.is_overview_open()
&& !mod_down
&& under.layer.is_none()
@@ -4222,16 +4274,10 @@ impl State {
if let Some(output) = self.niri.screenshot_ui.selection_output().cloned() {
let geom = self.niri.global_space.output_geometry(&output).unwrap();
let mut point = (pos - geom.loc.to_f64())
let point = (pos - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round::<i32>();
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
point.x = point.x.clamp(0, size.w - 1);
point.y = point.y.clamp(0, size.h - 1);
self.niri.screenshot_ui.pointer_motion(point, Some(slot));
self.niri.queue_redraw(&output);
}
@@ -4607,6 +4653,9 @@ fn allowed_during_screenshot(action: &Action) -> bool {
| Action::Suspend
| Action::PowerOffMonitors
| Action::PowerOnMonitors
// Intended for binds such as volume up/down, lock the screen, etc.
| Action::Spawn(_)
| Action::SpawnSh(_)
// The screenshot UI can handle these.
| Action::MoveColumnLeft
| Action::MoveColumnLeftOrToMonitorLeft
+6 -5
View File
@@ -345,16 +345,17 @@ impl<W: LayoutElement> FloatingSpace<W> {
compute_toplevel_bounds(border_config, self.working_area.size)
}
/// Returns the geometry of the active tile relative to and clamped to the working area.
/// Returns the geometry of the active window relative to and clamped to the working area.
///
/// During animations, assumes the final tile position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
let (tile, offset) = self.tiles_with_offsets().next()?;
let tile_size = tile.tile_size();
let tile_rect = Rectangle::new(offset, tile_size);
let window_pos = offset + tile.window_loc();
let window_size = tile.window_size();
let window_rect = Rectangle::new(window_pos, window_size);
self.working_area.intersection(tile_rect)
self.working_area.intersection(window_rect)
}
pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
+3 -3
View File
@@ -1342,15 +1342,15 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
/// Returns the geometry of the active tile relative to and clamped to the output.
/// Returns the geometry of the active window relative to and clamped to the output.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
if self.overview_open {
return None;
}
self.active_workspace_ref().active_tile_visual_rectangle()
self.active_workspace_ref().active_window_visual_rectangle()
}
fn workspace_size(&self, zoom: f64) -> Size<f64, Logical> {
+6 -6
View File
@@ -2540,10 +2540,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(hint_area)
}
/// Returns the geometry of the active tile relative to and clamped to the view.
/// Returns the geometry of the active window relative to and clamped to the view.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
let col = self.columns.get(self.active_column_idx)?;
let final_view_offset = self.view_offset.target();
@@ -2551,12 +2551,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let (tile, tile_off) = col.tiles().nth(col.active_tile_idx).unwrap();
let tile_pos = view_off + tile_off;
let tile_size = tile.tile_size();
let tile_rect = Rectangle::new(tile_pos, tile_size);
let window_pos = view_off + tile_off + tile.window_loc();
let window_size = tile.window_size();
let window_rect = Rectangle::new(window_pos, window_size);
let view = Rectangle::from_size(self.view_size);
view.intersection(tile_rect)
view.intersection(window_rect)
}
pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
+3 -3
View File
@@ -1609,11 +1609,11 @@ impl<W: LayoutElement> Workspace<W> {
floating.chain(scrolling)
}
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
pub fn active_window_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
if self.floating_is_active.get() {
self.floating.active_tile_visual_rectangle()
self.floating.active_window_visual_rectangle()
} else {
self.scrolling.active_tile_visual_rectangle()
self.scrolling.active_window_visual_rectangle()
}
}
+1 -1
View File
@@ -934,7 +934,7 @@ impl State {
let monitor = self.niri.layout.monitor_for_output(output).unwrap();
let mut rv = false;
let rect = monitor.active_tile_visual_rectangle();
let rect = monitor.active_window_visual_rectangle();
if let Some(rect) = rect {
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
+4 -4
View File
@@ -209,7 +209,7 @@ impl Blur {
let mut fbos = [0; 2];
gl.GenFramebuffers(fbos.len() as _, fbos.as_mut_ptr());
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbos[0]);
gl.BindFramebuffer(ffi::FRAMEBUFFER, fbos[0]);
let program = &self.program.0.down;
gl.UseProgram(program.program);
@@ -244,7 +244,7 @@ impl Blur {
trace!("drawing down {src} to {dst}");
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
dst,
@@ -307,7 +307,7 @@ impl Blur {
trace!("drawing up {src} to {dst}");
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
dst,
@@ -333,7 +333,7 @@ impl Blur {
gl.DisableVertexAttribArray(program.attrib_vert as u32);
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, 0);
gl.BindFramebuffer(ffi::FRAMEBUFFER, 0);
gl.DeleteFramebuffers(fbos.len() as _, fbos.as_ptr());
})?;
+34 -9
View File
@@ -799,6 +799,8 @@ impl ScreenshotUi {
}
/// The pointer has moved to `point` relative to the current selection output.
///
/// The point may be outside output bounds.
pub fn pointer_motion(&mut self, point: Point<i32, Physical>, slot: Option<TouchSlot>) {
let Self::Open {
selection,
@@ -838,7 +840,8 @@ impl ScreenshotUi {
selection.1 += delta;
selection.2 += delta;
} else {
selection.2 = point;
let size = output_data[&selection.0].size;
selection.2 = Point::new(point.x.clamp(0, size.w - 1), point.y.clamp(0, size.h - 1));
}
self.update_buffers();
@@ -849,6 +852,7 @@ impl ScreenshotUi {
output: Output,
point: Point<i32, Physical>,
slot: Option<TouchSlot>,
move_existing: bool,
) -> bool {
let Self::Open {
selection,
@@ -883,6 +887,23 @@ impl ScreenshotUi {
return false;
}
if move_existing {
if output != selection.0 {
return false;
}
*button = Button::Down {
touch_slot: slot,
on_capture_button: false,
last_pos: (output, point),
move_state: Some(MoveState {
pointer_offset: point - selection.1,
touch_slot: slot,
}),
};
return true;
}
let Some(output_data) = output_data.get(&output) else {
return false;
};
@@ -909,6 +930,11 @@ impl ScreenshotUi {
last_pos: (output.clone(), point),
move_state: None,
};
let point = Point::new(
point.x.clamp(0, output_data.size.w - 1),
point.y.clamp(0, output_data.size.h - 1),
);
*selection = (output, point, point);
self.update_buffers();
@@ -939,15 +965,14 @@ impl ScreenshotUi {
return None;
};
// Check if this is a move touch and if so, stop the move.
if let Some(state) = move_state {
if state.touch_slot.is_some_and(|m_slot| Some(m_slot) == slot) {
*move_state = None;
return None;
}
};
if touch_slot != slot {
// This is not our main touch, but it might be the move touch. If so, stop the move.
if let Some(state) = move_state {
if state.touch_slot.is_some_and(|m_slot| Some(m_slot) == slot) {
*move_state = None;
}
};
return None;
}