diff --git a/kick.c b/kick.c index fa5ec5a..b63ea7d 100644 --- a/kick.c +++ b/kick.c @@ -1053,6 +1053,10 @@ static struct kick_window *tab_current_window(struct kick_tab *tab); static bool overlay_hit_test(double ox, double oy, struct kick_tab **tab_out, struct kick_window **win_out); +/* True while pointer motion/buttons must not reach clients: the Alt overlay + is up (hover previews tabs, clicks are swallowed). Defined with the + touch-selection state; called from the pointer paths above it. */ +static bool overlay_eats_pointer(void); /* Toplevel-icon body lives with the overlay code; called from teardown. */ static void icon_proto_release(struct kick_window *w); /* Toplevel-tag bodies live with the foreign-handle section; called from @@ -10665,6 +10669,12 @@ static void standalone_ptr_button_event(struct wl_listener *listener, void *data if (!server.seat) return; if (server.idle_notifier) wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat); + /* Clicks while the overlay is up are swallowed: hover already previewed, + and the client beneath must not see a press/release pair. */ + if (overlay_eats_pointer()) { + wlr_seat_pointer_notify_frame(server.seat); + return; + } wlr_seat_pointer_notify_button(server.seat, ev->time_msec, ev->button, ev->state); wlr_seat_pointer_notify_frame(server.seat); @@ -10898,8 +10908,13 @@ static void ctl_dispatch(const uint8_t *msg, size_t len) { ctl_time += 1; /* Button press/release at the current pointer position, through the same seat entry real devices use. Button codes are evdev-style - (BTN_LEFT = 0x110); state 1 = press, 0 = release. */ + (BTN_LEFT = 0x110); state 1 = press, 0 = release. Swallowed + while the overlay is up, like every other button path. */ if (!server.seat) return; + if (overlay_eats_pointer()) { + wlr_seat_pointer_notify_frame(server.seat); + return; + } wlr_seat_pointer_notify_button(server.seat, ctl_time, u32[0], u32[1]); wlr_seat_pointer_notify_frame(server.seat); @@ -11124,21 +11139,26 @@ static void overlay_touch_release(int32_t id) { overlay_touch_driver = -1; } -/* A tap on an overlay chip while the prefix is held: preview that window - (the release commits it to the MRU front via the usual overlay path). - Mirrors mru_step's set_active_tab + window_enter pinning so a stacked - member is selected exactly, not just its tab's current window. Re-taps - (or drag motion) landing on the already-shown window are a no-op, so - motion-rate events never spam the log. */ -static void overlay_touch_select(struct kick_window *w) { - if (!w || server.session_locked) return; - if (w == mru_current()) return; +/* Preview a window while the overlay is up (the Alt release commits it to + the MRU front via the usual overlay path). Shared by touch taps/drags + and pointer hover. Mirrors mru_step's set_active_tab + window_enter + pinning so a stacked member is selected exactly, not just its tab's + current window. Landing on the already-shown window is a no-op (false), + so motion-rate events never spam the log; callers log on true. */ +static bool overlay_preview_select(struct kick_window *w) { + if (!w || server.session_locked) return false; + if (w == mru_current()) return false; struct kick_tab *tab = w->tab; - if (!tab || !tab_overlay_row(tab)) return; + if (!tab || !tab_overlay_row(tab)) return false; if (tab != server.active_tab) set_active_tab(tab); window_enter(w); - kick_log(KICK_LOG_INFO, "[touch] select app=%d\n", tab_index(tab)); + return true; +} + +static bool overlay_eats_pointer(void) { + return host.overlay_visible && !confirm_quit_pending && + !server.session_locked; } static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action, @@ -11159,8 +11179,10 @@ static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action, if (overlay_touch_driver == -1) { struct kick_tab *ht = NULL; struct kick_window *hw = NULL; - if (overlay_hit_test(ox, oy, &ht, &hw) && hw) - overlay_touch_select(hw); + if (overlay_hit_test(ox, oy, &ht, &hw) && hw && + overlay_preview_select(hw)) + kick_log(KICK_LOG_INFO, "[touch] select app=%d\n", + tab_index(ht)); overlay_touch_consume(id); overlay_touch_driver = id; } else { @@ -11176,8 +11198,10 @@ static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action, if (id == overlay_touch_driver) { struct kick_tab *ht = NULL; struct kick_window *hw = NULL; - if (overlay_hit_test(ox, oy, &ht, &hw) && hw) - overlay_touch_select(hw); + if (overlay_hit_test(ox, oy, &ht, &hw) && hw && + overlay_preview_select(hw)) + kick_log(KICK_LOG_INFO, "[touch] select app=%d\n", + tab_index(ht)); } } else { overlay_touch_release(id); @@ -11243,6 +11267,19 @@ static void host_ptr_motion(uint32_t time, double sx, double sy) { wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat); server.last_ptr_x = sx; server.last_ptr_y = sy; + /* Hovering the overlay previews rows like a touch drag (no click + needed); motion never reaches clients while it is up. Raw coords: + the overlay sits outside any constrained surface. */ + if (overlay_eats_pointer()) { + struct kick_tab *ht = NULL; + struct kick_window *hw = NULL; + if (overlay_hit_test(sx, sy, &ht, &hw) && hw && + overlay_preview_select(hw)) + kick_log(KICK_LOG_INFO, "[pointer] select app=%d\n", + tab_index(ht)); + wlr_seat_pointer_notify_frame(server.seat); + return; + } constraint_filter_abs(&sx, &sy); double nx = 0.0, ny = 0.0; struct wlr_surface *surface = surface_at(sx, sy, &nx, &ny); @@ -11294,6 +11331,10 @@ static void host_ptr_button(void *data, struct wl_pointer *p, uint32_t serial, if (!server.seat) return; if (server.idle_notifier) wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat); + if (overlay_eats_pointer()) { + wlr_seat_pointer_notify_frame(server.seat); + return; + } wlr_seat_pointer_notify_button(server.seat, time, button, state); wlr_seat_pointer_notify_frame(server.seat); } diff --git a/tests/test_smoke.py b/tests/test_smoke.py index e3f4d5b..81c9e97 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -4962,6 +4962,91 @@ def test_overlay_touch_drag(session): assert session.active() == drag_target +def test_overlay_pointer_hover(session): + """Moving the cursor over the overlay while Alt is held previews rows + like a touch drag (no click needed); motion off every row sticks to + the last preview; clicks are swallowed (no new selection, no active + change, nothing reaches the client); Alt release commits.""" + session.wait_any_active() + session.wait_surface_mapped(0) + session.wait_surface_mapped(1) + session.mods(ALT) + try: + path = session.dump_host() + w, h, _ = session.ppm_rgb(path) + cx = w // 2 + # Locate both rows by warping down the middle. + rows = {} + deadline = time.monotonic() + 25.0 + y = 0 + while time.monotonic() < deadline and len(rows) < 2 and y < h: + before = len(re.findall(r"\[pointer\] select app=", session.read_log())) + session.warp(cx, y) + time.sleep(0.18) + m = re.findall(r"\[pointer\] select app=(\d+)", session.read_log()) + if len(m) > before and m[-1] not in rows: + rows[m[-1]] = y + y += 20 + assert len(rows) == 2, f"hover sweep found rows {rows}" + yA, yB = sorted(rows.values()) + # Hover from the upper row into the lower one. + session.warp(cx, yA) + time.sleep(0.3) + session.warp(cx, yB) + time.sleep(0.3) + sels = re.findall(r"\[pointer\] select app=(\d+)", session.read_log()) + assert len(sels) >= 2 and sels[-2] != sels[-1], \ + f"hover never re-targeted: {sels[-2:]}" + hover_target = int(sels[-1]) + # Motion off every row keeps the last preview. + n0 = len(re.findall(r"\[pointer\] select app=", session.read_log())) + session.warp(5, 5) + time.sleep(0.3) + n1 = len(re.findall(r"\[pointer\] select app=", session.read_log())) + assert n1 == n0, "off-row hover changed selection" + # Clicks are swallowed: no selection, no active change. + a0 = session.active_count() + session.button(BTN_LEFT, 1) + time.sleep(0.15) + session.button(BTN_LEFT, 0) + time.sleep(0.3) + n2 = len(re.findall(r"\[pointer\] select app=", session.read_log())) + assert n2 == n1, "click while overlay up selected a window" + assert session.active_count() == a0, "click while overlay up moved focus" + assert session.active() == hover_target + finally: + session.mods(0) + session.wait_for(r"\[mru\] commit app=%d" % hover_target, timeout=10.0) + assert session.active() == hover_target + + +def test_overlay_pointer_click_swallowed(tmp_path): + """Clicks while the overlay is up never reach clients: a wltest client + with --pointer-out sees buttons with the overlay down and nothing + new while Alt is held.""" + out = tmp_path / "pointer.out" + with _run_kick( + ["%s --pointer-out '%s' '#ff0000'" % (WL, out)]) as s: + s.wait_any_active() + s.wait_surface_mapped(0) + path = s.dump_host() + w, h, _ = s.ppm_rgb(path) + s.warp(w // 2, h // 2) + s.click() + assert _wait_out(out, "button 272 1", timeout=10.0), \ + f"client saw no click with overlay down: {out.read_text()!r}" + n0 = out.read_text().count("button") + s.mods(ALT) + try: + time.sleep(0.7) + s.click() + time.sleep(0.8) + finally: + s.mods(0) + n1 = out.read_text().count("button") + assert n1 == n0, f"click leaked to client while overlay up: {out.read_text()!r}" + + def test_synth_feed_drives_host_listeners(tmp_path): """Synthesized host pointer/touch events go through the real host wl_pointer/wl_touch listeners instead of bypassing them for the shared