gpui: Add input region support for Wayland windows (#60161)

# Objective

Wayland windows have no way to restrict which parts of the surface
accept pointer and touch input. This adds support for setting an input
region, so events outside it pass through to whatever is below the
window. This is useful for shaped or partially click-through windows.


Clicks in green area can pass through the window, clicks in red area do
not:
<img width="1057" height="395" alt="image"
src="https://github.com/user-attachments/assets/2039af62-e43b-4834-b877-edad2a8f5ccf"
/>

## Solution

Add `Window::set_input_region`, which takes `Option<&[Bounds<Pixels>]>`:

- `Some(rects)` restricts pointer and touch input to the union of the
rectangles, in window coordinates.
- `Some(&[])` is an empty region, so the window receives no input at all
and is fully click-through.
- `None` resets the region to the default, so the whole window receives
input again.

On Wayland this maps to `wl_surface.set_input_region`, building a
`wl_region` from the rectangles or clearing it for `None`, and commits
so the change applies immediately rather than waiting for the next
frame. The method is a no-op on other platforms.


## Testing

Tested on Linux with Wayland.

- Tested in my own GPUI application, which uses a fullscreen layer for
overlays while allowing clicks outside of the rendered elements to be
passed through to the underlying windows.
- No automated test was added, since this calls through to the
compositor and is checked by observing input routing.


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
This commit is contained in:
Philipp Schaffrath 2026-07-07 10:54:07 +02:00 committed by GitHub
parent fcd0f76952
commit a29c0d41f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 43 additions and 0 deletions

View file

@ -702,6 +702,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server
}

View file

@ -1999,6 +1999,16 @@ impl Window {
self.platform_window.start_window_resize(edge);
}
/// Linux (wayland) only: Set the window's input region, the area that receives pointer
/// and touch input. Events outside it pass through to whatever is below the window.
///
/// - `Some(rects)` restricts input to the union of `rects`, in window coordinates.
/// - `Some(&[])` is an empty region, so the window receives no pointer or touch input.
/// - `None` resets the region to the default, so the whole window receives input again.
pub fn set_input_region(&self, region: Option<&[Bounds<Pixels>]>) {
self.platform_window.set_input_region(region);
}
/// Return the `WindowBounds` to indicate that how a window should be opened
/// after it has been closed
pub fn window_bounds(&self) -> WindowBounds {

View file

@ -1489,6 +1489,38 @@ impl PlatformWindow for WaylandWindow {
}
}
fn set_input_region(&self, region: Option<&[Bounds<Pixels>]>) {
let state = self.borrow();
match region {
// No region means the whole surface receives input.
None => state.surface.set_input_region(None),
// A region restricts input to its rectangles. An empty region
// receives no input at all.
Some(rects) => {
let wl_region = state
.globals
.compositor
.create_region(&state.globals.qh, ());
for rect in rects {
let rect = rect.map(|pixels| f32::from(pixels) as i32);
wl_region.add(
rect.origin.x,
rect.origin.y,
rect.size.width,
rect.size.height,
);
}
state.surface.set_input_region(Some(&wl_region));
wl_region.destroy();
}
}
// Commit so the new input region applies immediately. Otherwise it
// waits for the next frame, which could be the very click we want to
// allow passing through.
state.surface.commit();
}
fn window_decorations(&self) -> Decorations {
let state = self.borrow();
match state.decorations {