feat: drag across overlay retargets touch selection

This commit is contained in:
Andrew Hamilton 2026-09-16 21:36:57 +10:00
parent 0d65efdd49
commit d91cc8ba8d
2 changed files with 105 additions and 11 deletions

51
kick.c
View file

@ -11097,6 +11097,10 @@ static size_t touch_slot_count;
motion/up must not leak to clients as a partial gesture. */
static int32_t overlay_touch_slots[KICK_TOUCH_MAX_SLOTS];
static size_t overlay_touch_count;
/* First touch id that went down while the overlay was up: the only finger
whose motion re-targets the preview (first-touch-wins). -1 when no
overlay drag is active. */
static int32_t overlay_touch_driver = -1;
static bool overlay_touch_consumed(int32_t id) {
for (size_t i = 0; i < overlay_touch_count; i++)
@ -11114,16 +11118,21 @@ static void overlay_touch_release(int32_t id) {
for (size_t i = 0; i < overlay_touch_count; i++)
if (overlay_touch_slots[i] == id) {
overlay_touch_slots[i] = overlay_touch_slots[--overlay_touch_count];
return;
break;
}
if (id == overlay_touch_driver)
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. */
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;
struct kick_tab *tab = w->tab;
if (!tab || !tab_overlay_row(tab)) return;
if (tab != server.active_tab)
@ -11138,22 +11147,41 @@ static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action,
if (server.idle_notifier)
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
/* While the Alt overlay is up, touches never reach clients: a tap on
a row previews that window (single tap selects), anything else is
ignored. Checked before constraint clamping -- the overlay sits
outside any constrained surface. */
a row previews that window (single tap selects, drag motion
re-targets the preview as the finger slides between titles),
anything else is ignored. Checked before constraint clamping --
the overlay sits outside any constrained surface. */
if (host.overlay_visible && !confirm_quit_pending && !server.session_locked) {
if (action == KICK_TOUCH_DOWN) {
struct kick_tab *ht = NULL;
struct kick_window *hw = NULL;
if (overlay_hit_test(ox, oy, &ht, &hw) && hw)
overlay_touch_select(hw);
overlay_touch_consume(id);
/* First-touch-wins: only the finger that claims the idle
driver previews; concurrent fingers are swallowed silently
(and never promote when the driver lifts, to avoid jumps). */
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);
overlay_touch_consume(id);
overlay_touch_driver = id;
} else {
overlay_touch_consume(id);
}
wlr_seat_touch_notify_frame(server.seat);
return;
}
if (overlay_touch_consumed(id)) {
if (action != KICK_TOUCH_MOTION)
if (action == KICK_TOUCH_MOTION) {
/* Drag: sliding into another row's title previews it;
sliding out of every row sticks to the last preview. */
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);
}
} else {
overlay_touch_release(id);
}
wlr_seat_touch_notify_frame(server.seat);
return;
}
@ -11203,6 +11231,7 @@ static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action,
static void seat_touch_cancel_all(void) {
/* Overlay-swallowed touches were never forwarded: just drop them. */
overlay_touch_count = 0;
overlay_touch_driver = -1;
while (touch_slot_count > 0)
seat_touch_deliver(0, touch_slots[touch_slot_count - 1],
KICK_TOUCH_UP, 0, 0);

View file

@ -4897,6 +4897,71 @@ def test_overlay_touch_select(session):
assert session.active() == target
def test_overlay_touch_drag(session):
"""Dragging a finger across the overlay while Alt is held re-targets
the preview as the touch slides into each row's titles; sliding out
of every row sticks to the last preview; a second finger never steals
the drag; lifting commits nothing (Alt release still 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 tapping 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"\[touch\] select app=", session.read_log()))
session.touch(7, TOUCH_DOWN, cx, y)
time.sleep(0.05)
session.touch(7, TOUCH_UP, cx, y)
time.sleep(0.12)
m = re.findall(r"\[touch\] 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"touch sweep found rows {rows}"
yA, yB = sorted(rows.values())
# Drag from the upper row into the lower one step by step.
base = len(re.findall(r"\[touch\] select app=", session.read_log()))
session.touch(7, TOUCH_DOWN, cx, yA)
time.sleep(0.2)
for yy in range(yA, yB + 1, 10):
session.touch(7, TOUCH_MOTION, cx, yy)
time.sleep(0.03)
time.sleep(0.3)
sels = re.findall(r"\[touch\] select app=(\d+)", session.read_log())
assert len(sels) > base and sels[base] != sels[-1], \
f"drag never re-targeted: {sels[base:]}"
drag_target = int(sels[-1])
# Sliding out of every row keeps the last preview.
n0 = len(re.findall(r"\[touch\] select app=", session.read_log()))
session.touch(7, TOUCH_MOTION, 5, 5)
time.sleep(0.3)
n1 = len(re.findall(r"\[touch\] select app=", session.read_log()))
assert n1 == n0, "off-row drag motion changed selection"
# A second finger's down+motion never steals the drag.
session.touch(9, TOUCH_DOWN, cx, yA)
time.sleep(0.1)
session.touch(9, TOUCH_MOTION, cx, yB)
time.sleep(0.2)
n2 = len(re.findall(r"\[touch\] select app=", session.read_log()))
session.touch(9, TOUCH_UP, cx, yB)
time.sleep(0.1)
assert n2 == n1, "second finger stole the overlay drag"
session.touch(7, TOUCH_UP, cx, yB)
time.sleep(0.2)
assert session.active() == drag_target
finally:
session.mods(0)
session.wait_for(r"\[mru\] commit app=%d" % drag_target, timeout=10.0)
assert session.active() == drag_target
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