feat: select overlay windows with the mouse cursor

Moving the cursor over an overlay row now previews that window, the
same as dragging a finger across it; sliding off every row keeps the
last preview. Clicks while the overlay is up do nothing and never
reach clients, and releasing Alt commits the preview.
This commit is contained in:
Andrew Hamilton 2026-09-16 22:00:30 +10:00
parent d91cc8ba8d
commit a4e5633a27
2 changed files with 142 additions and 16 deletions

73
kick.c
View file

@ -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);
}

View file

@ -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