fix(wayland): guard null focusedWindow in pointer handlers

Sway delivers pointer events in surprising orders around input-device
hot-plug — observed: a second wl_pointer.leave after focus was already
cleared, which crashed in PointerListenerHandleLeave (null deref), and
motion/button would crash the same way if an enter never matched one of
our surfaces. Early-return instead. Found while end-to-end-testing the
scroll chain for #32 with a zwlr_virtual_pointer_v1 injector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-12 15:16:57 +00:00
commit 2402f35e6c

View file

@ -223,6 +223,11 @@ void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t na
}
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
// Defensive: compositors can deliver pointer events without (or after)
// a matching enter — observed on sway during input-device hot-plug.
if (focusedWindow == nullptr) {
return;
}
if (button == BTN_LEFT) {
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
Device::focusedWindow->mouseLeftHeld = true;
@ -243,6 +248,9 @@ void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_
}
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
if (focusedWindow == nullptr) {
return;
}
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
@ -267,8 +275,12 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
}
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
Device::focusedWindow->onMouseLeave.Invoke();
focusedWindow = nullptr;
// Sway has been observed to send a second leave after focus is already
// gone (input-device hot-plug) — don't crash on it.
if (focusedWindow != nullptr) {
focusedWindow->onMouseLeave.Invoke();
focusedWindow = nullptr;
}
// Drop any sub-detent scroll remainder so it can't leak into whatever
// window the pointer enters next.
scrollDetentRemainder = 0.0;