Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/wiki/Configuration:-Window-Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,32 @@ window-rule {
> This is because window title (and app ID) are not double-buffered in the Wayland protocol, so they are not tied to specific window contents.
> There's no robust way for Firefox to synchronize visibly showing a different tab and changing the window title.

#### `block-pointer-constraints`

<sup>Since: next release</sup>

Suppress activation of [`zwp_pointer_constraints_v1`](https://wayland.app/protocols/pointer-constraints-unstable-v1) requests originating from this window.
Affects both `locked` and `confined` constraint variants.

The constraint can still be requested and bound to the window's surfaces by the client.
niri simply never activates it, so the client observes a constraint that remains inactive for its entire lifetime.
Well-behaved clients gate relative-motion mode and similar features on the constraint being active, so they degrade gracefully to normal absolute-motion input.

Useful for opting out of apps that request a pointer-lock as part of a tooltip, annotation, or overlay UI that the user doesn't want to engage when the cursor merely crosses the surface.

For example, Zoom's `annotate_toolbar` overlay window requests a pointer-lock the user typically doesn't want to engage:

```kdl
window-rule {
match app-id="^Zoom$" title="^annotate_toolbar$"
block-pointer-constraints true
}
```

> [!NOTE]
> This is unrelated to keyboard focus or pointer focus.
> Cursor entry into the window's surface still updates pointer focus normally (and triggers [`focus-follows-mouse`](./Configuration:-Input.md#focus-follows-mouse) if configured) — only the pointer-lock/confine activation is gated.

#### `opacity`

Set the opacity of the window.
Expand Down
4 changes: 4 additions & 0 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ mod tests {
default-window-height { fixed 500; }
default-column-display "tabbed"
default-floating-position x=100 y=-200 relative-to="bottom-left"
block-pointer-constraints true
focus-ring {
off
Expand Down Expand Up @@ -1857,6 +1858,9 @@ mod tests {
clip_to_geometry: None,
baba_is_float: None,
block_out_from: None,
block_pointer_constraints: Some(
true,
),
variable_refresh_rate: None,
default_column_display: Some(
Tabbed,
Expand Down
2 changes: 2 additions & 0 deletions niri-config/src/window_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub struct WindowRule {
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, unwrap(argument))]
pub block_pointer_constraints: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub variable_refresh_rate: Option<bool>,
#[knuffel(child, unwrap(argument, str))]
pub default_column_display: Option<ColumnDisplay>,
Expand Down
16 changes: 16 additions & 0 deletions src/niri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6080,6 +6080,22 @@ impl Niri {
return;
}

// Window-rule opt-out: a window matched by a rule with
// `block-pointer-constraints true` short-circuits before
// activation. The constraint stays bound but inactive for its
// lifetime, so the protocol behaves like a silent no-op.
// Resolving root + looking up the window rules is deferred
// until here so the no-constraint fast path (every pointer
// motion over a surface without a constraint) doesn't pay
// for it. Pointer-constraints can be requested on subsurfaces
// / popups, but rules resolve on the toplevel.
let root = self.find_root_shell_surface(surface);
if let Some((mapped, _)) = self.layout.find_window_and_output(&root) {
if mapped.rules().block_pointer_constraints == Some(true) {
return;
}
}

// Constraint does not apply if not within region.
if let Some(region) = constraint.region() {
let pointer_pos = pointer.current_location();
Expand Down
6 changes: 6 additions & 0 deletions src/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ pub struct ResolvedWindowRules {
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,

/// Whether to block pointer-constraints (locked + confined) for this window.
pub block_pointer_constraints: Option<bool>,

/// Whether to enable VRR on this window's primary output if it is on-demand.
pub variable_refresh_rate: Option<bool>,

Expand Down Expand Up @@ -293,6 +296,9 @@ impl ResolvedWindowRules {
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
if let Some(x) = rule.block_pointer_constraints {
resolved.block_pointer_constraints = Some(x);
}
if let Some(x) = rule.variable_refresh_rate {
resolved.variable_refresh_rate = Some(x);
}
Expand Down