Skip to content

fix: prioritize activation region hits over CSD overflow in scrolling hit testing#3453

Open
odtgit wants to merge 1 commit intoniri-wm:mainfrom
odtgit:fix/csd-edge-panning-input-steal
Open

fix: prioritize activation region hits over CSD overflow in scrolling hit testing#3453
odtgit wants to merge 1 commit intoniri-wm:mainfrom
odtgit:fix/csd-edge-panning-input-steal

Conversation

@odtgit
Copy link

@odtgit odtgit commented Feb 15, 2026

what this fixes

focus-follows-mouse edge panning doesn't work reliably with CSD windows. with struts enabled and two 0.5-proportion columns side by side, moving the mouse to the screen edge should focus the adjacent peeking column - but CSD windows like firefox and alacritty block it most of the time, while SSD windows work fine.

root cause

CSD windows have buffers and input regions that extend beyond their geometry() bounds (shadows, resize handles etc). in ScrollingSpace::window_under(), tiles are checked in render order and the first tile whose hit() returns Some wins. since hit() checks is_in_input_region() first - which delegates to the wayland client's input region without clipping to tile bounds - the CSD shadow bleeds into the strut zone, the active column steals the hit, focus-follows-mouse sees the same window already focused, and nothing happens.

SSD windows don't have this issue because their geometry().loc is (0,0) and their buffer doesn't extend beyond the geometry.

worth noting that clip-to-geometry only affects rendering, not input - so it can't help here either.

the fix

instead of unconditionally clipping all pointer input to the activation region (which would break legitimate CSD surfaces extending beyond tile bounds, like GTK 3 subsurface popups or non-grabbing popups), this uses a two-pass approach in ScrollingSpace::window_under():

  1. first pass - only accept hits within the tile's activation region (tile bounds). this means the peeking column wins over the active column's CSD shadow in strut zones.
  2. second pass - fall back to the full hit() including CSD overflow regions. this preserves normal pointer input on legitimate surfaces extending beyond tile bounds.

the first pass almost always matches (pointer is within some tile's bounds), so the second pass only runs when the pointer is in a CSD overflow zone outside all tile bounds - negligible performance impact.

adds Tile::hit_within_activation_region() which checks activation region first, then input region within it. the original Tile::hit() is unchanged.

also updated resize_edges_under() in workspace.rs to use hit_within_activation_region() for consistency - it had a comment explicitly noting it should match window_under() behavior, and resize edge calculations are relative to tile geometry so CSD overflow hits would produce meaningless edges anyway.

what about other hit testing paths

  • floating windows - use single-pass hit_tile(). CSD overlap between floating windows is a z-order concern, not a strut/peeking issue. left as-is.
  • interactive move - uses single-pass hit_tile() on the single dragged tile. no overlap with itself, no change needed.
  • overview mode - converts all hits to Activate and disables resize. unaffected.
  • tab indicators - checked before tiles in the first pass so they win correctly. not rechecked in the second pass, but tab indicators don't extend into other columns' CSD zones in practice.

tested with

same tight layout - 4px gaps, 2px struts, 2px focus ring border, clip-to-geometry true, prefer-no-csd on:

layout {
    gaps 4
    struts {
        left 2
        right 2
    }
    default-column-width { proportion 0.5; }
    border {
        width 2
    }
}
  • edge panning via focus-follows-mouse works consistently on CSD windows (firefox, alacritty, chromium, discord, spotify)
  • normal pointer input on CSD regions extending beyond tile bounds (shadows, resize handles) still works
  • mod+right-click resize behaves correctly at tile edges
  • SSD windows unaffected (hit() and hit_within_activation_region() are equivalent for them)
  • also tested with different gap/strut/border geometries - all working

tried to test for regressions with the second pass (GTK 3 subsurface popups, non-grabbing popups) but couldn't find a good client that reliably produces CSD surfaces extending beyond tile bounds in a way that would exercise the fallback path. if anyone has a test case for that I'm happy to verify.

would definitely benefit from further testing, but since this enables a purely mouse-driven horizontal scrolling use-case that was broken before, I think it's worth checking out.

@Sempyos
Copy link
Member

Sempyos commented Feb 15, 2026

Have you tried using negative struts instead..?

@odtgit
Copy link
Author

odtgit commented Feb 15, 2026 via email

@YaLTeR
Copy link
Member

YaLTeR commented Feb 15, 2026

The problem is that there can be a legit part of a surface that sticks out. Most popups should be fine as they grab pointer input, but what about nongrabbing popups? (I'm not sure if there's a good test client, but I think if you unconditionally return from the xdg toplevel grab() handler then GTK popups will stick around without a grab.) Some clients use subsurfaces for popups, most notably GTK 3. You can tell by them getting clipped with clip-to-geometry. This isn't good Wayland behavior, but it's behavior that exists and works, and it would be weird if those parts of the surface just didn't accept input.

Perhaps restricting focus-follows-mouse specifically (and not normal pointer input) from triggering outside the activation region is better?

@odtgit odtgit force-pushed the fix/csd-edge-panning-input-steal branch from 01a7500 to 0427226 Compare February 15, 2026 20:20
@odtgit odtgit changed the title fix: prevent CSD shadow input regions from stealing edge panning hits fix: prioritize activation region hits over CSD overflow in scrolling hit testing Feb 15, 2026
@odtgit odtgit force-pushed the fix/csd-edge-panning-input-steal branch from 0427226 to 305c28a Compare February 15, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants