diff --git a/docs/wiki/Configuration:-Window-Rules.md b/docs/wiki/Configuration:-Window-Rules.md index f8fa215ec3..6fd36fb344 100644 --- a/docs/wiki/Configuration:-Window-Rules.md +++ b/docs/wiki/Configuration:-Window-Rules.md @@ -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` + +Since: next release + +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. diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 909aeb80a5..6f128c71bf 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -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 @@ -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, diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs index f2bc2ad157..a6619c3088 100644 --- a/niri-config/src/window_rule.rs +++ b/niri-config/src/window_rule.rs @@ -66,6 +66,8 @@ pub struct WindowRule { #[knuffel(child, unwrap(argument))] pub block_out_from: Option, #[knuffel(child, unwrap(argument))] + pub block_pointer_constraints: Option, + #[knuffel(child, unwrap(argument))] pub variable_refresh_rate: Option, #[knuffel(child, unwrap(argument, str))] pub default_column_display: Option, diff --git a/src/niri.rs b/src/niri.rs index 190ef09d55..2bc60ead12 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -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(); diff --git a/src/window/mod.rs b/src/window/mod.rs index 14527cd242..ad52a56eb1 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -111,6 +111,9 @@ pub struct ResolvedWindowRules { /// Whether to block out this window from certain render targets. pub block_out_from: Option, + /// Whether to block pointer-constraints (locked + confined) for this window. + pub block_pointer_constraints: Option, + /// Whether to enable VRR on this window's primary output if it is on-demand. pub variable_refresh_rate: Option, @@ -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); }