From 12adb678bbe1bec258b55a61ce8bc87f05051bfd Mon Sep 17 00:00:00 2001 From: entailz Date: Wed, 20 May 2026 18:48:48 -0700 Subject: [PATCH] accepts_input_at rejects buffer-less surfaces --- src/ifs/wl_surface.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 547b7e2a..e9ae9d7b 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -318,7 +318,7 @@ pub struct WlSurface { pub content_type: Cell>, pub drm_feedback: CopyHashMap>, syncobj_surface: CloneCell>>, - destroyed: Cell, + pub destroyed: Cell, commit_timeline: CommitTimeline, alpha_modifier: CloneCell>>, alpha: Cell>, @@ -1019,6 +1019,7 @@ impl WlSurfaceRequestHandler for WlSurface { self.unset_dnd_icons(); self.unset_cursors(); self.ext.get().on_surface_destroy()?; + self.destroyed.set(true); self.destroy_node(); { let mut children = self.children.borrow_mut(); @@ -1029,6 +1030,19 @@ impl WlSurfaceRequestHandler for WlSurface { } *children = None; } + // Capture a close-animation snapshot if the client is destroying the + // surface while it still has a buffer (i.e. without a clean null-attach + // commit first — typical for crash/disconnect paths). + if self.buffer.is_some() + && let Some(tl) = self.toplevel.get() + && let Some(snap) = crate::animation::capture_snapshot(&self.client.state, &tl) + { + self.client + .state + .close_snapshots + .borrow_mut() + .push(Rc::new(snap)); + } self.buffer.set(None); self.reset_shm_textures(); if let Some(xwayland_serial) = self.xwayland_serial.get() { @@ -1041,7 +1055,6 @@ impl WlSurfaceRequestHandler for WlSurface { self.client.remove_obj(self)?; self.idle_inhibitors.clear(); self.constraints.take(); - self.destroyed.set(true); Ok(()) } @@ -1238,8 +1251,24 @@ impl WlSurface { let mut buffer_changed = false; let mut old_raw_size = None; let (mut dx, mut dy) = mem::take(&mut pending.offset); + let mut buffer_presence_changed = false; if let Some(buffer_change) = pending.buffer.take() { buffer_changed = true; + buffer_presence_changed = buffer_change.is_some() != self.buffer.is_some(); + // If the client just attached a null buffer to the main surface of + // a mapped toplevel, capture a snapshot before we drop the buffer + // so the close animation has something to render after teardown. + if buffer_change.is_none() + && self.buffer.is_some() + && let Some(tl) = self.toplevel.get() + && let Some(snap) = crate::animation::capture_snapshot(&self.client.state, &tl) + { + self.client + .state + .close_snapshots + .borrow_mut() + .push(Rc::new(snap)); + } if let Some(buffer) = self.buffer.take() { old_raw_size = Some(buffer.buffer.buf.rect); } @@ -1408,6 +1437,16 @@ impl WlSurface { }; self.is_opaque.set(is_opaque); } + if buffer_abs_pos_size_changed || buffer_presence_changed { + // Pointer focus depends on whether this surface accepts input. + // It just changed (size shrank/grew, or buffer went from null to + // non-null or vice versa — the latter happens when a client + // dismisses a subsurface by null-attaching while keeping its + // wp_viewport destination). Force a re-evaluation so the pointer + // stack doesn't keep a now-invisible surface focused until the + // next motion event. + self.client.state.tree_changed(); + } let mut tearing_changed = false; if let Some(tearing) = pending.tearing.take() && self.tearing.replace(tearing) != tearing @@ -1597,6 +1636,13 @@ impl WlSurface { } fn accepts_input_at(&self, mut x: i32, mut y: i32) -> bool { + // Per the wayland spec, a surface without a buffer is invisible and + // cannot receive input. Without this check, a client that null-buffers + // but keeps a wp_viewport destination set (as foot does for its + // fractional-scaling subsurfaces) would keep an invisible hit-rect. + if self.buffer.is_none() { + return false; + } let rect = self.buffer_abs_pos.get().at_point(0, 0); if !rect.contains(x, y) { return false;