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

41
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. */ motion/up must not leak to clients as a partial gesture. */
static int32_t overlay_touch_slots[KICK_TOUCH_MAX_SLOTS]; static int32_t overlay_touch_slots[KICK_TOUCH_MAX_SLOTS];
static size_t overlay_touch_count; 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) { static bool overlay_touch_consumed(int32_t id) {
for (size_t i = 0; i < overlay_touch_count; i++) 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++) for (size_t i = 0; i < overlay_touch_count; i++)
if (overlay_touch_slots[i] == id) { if (overlay_touch_slots[i] == id) {
overlay_touch_slots[i] = overlay_touch_slots[--overlay_touch_count]; 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 /* 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). (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 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) { static void overlay_touch_select(struct kick_window *w) {
if (!w || server.session_locked) return; if (!w || server.session_locked) return;
if (w == mru_current()) return;
struct kick_tab *tab = w->tab; struct kick_tab *tab = w->tab;
if (!tab || !tab_overlay_row(tab)) return; if (!tab || !tab_overlay_row(tab)) return;
if (tab != server.active_tab) 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) if (server.idle_notifier)
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat); wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
/* While the Alt overlay is up, touches never reach clients: a tap on /* While the Alt overlay is up, touches never reach clients: a tap on
a row previews that window (single tap selects), anything else is a row previews that window (single tap selects, drag motion
ignored. Checked before constraint clamping -- the overlay sits re-targets the preview as the finger slides between titles),
outside any constrained surface. */ 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 (host.overlay_visible && !confirm_quit_pending && !server.session_locked) {
if (action == KICK_TOUCH_DOWN) { if (action == KICK_TOUCH_DOWN) {
/* 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_tab *ht = NULL;
struct kick_window *hw = NULL; struct kick_window *hw = NULL;
if (overlay_hit_test(ox, oy, &ht, &hw) && hw) if (overlay_hit_test(ox, oy, &ht, &hw) && hw)
overlay_touch_select(hw); overlay_touch_select(hw);
overlay_touch_consume(id); overlay_touch_consume(id);
overlay_touch_driver = id;
} else {
overlay_touch_consume(id);
}
wlr_seat_touch_notify_frame(server.seat); wlr_seat_touch_notify_frame(server.seat);
return; return;
} }
if (overlay_touch_consumed(id)) { 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); overlay_touch_release(id);
}
wlr_seat_touch_notify_frame(server.seat); wlr_seat_touch_notify_frame(server.seat);
return; 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) { static void seat_touch_cancel_all(void) {
/* Overlay-swallowed touches were never forwarded: just drop them. */ /* Overlay-swallowed touches were never forwarded: just drop them. */
overlay_touch_count = 0; overlay_touch_count = 0;
overlay_touch_driver = -1;
while (touch_slot_count > 0) while (touch_slot_count > 0)
seat_touch_deliver(0, touch_slots[touch_slot_count - 1], seat_touch_deliver(0, touch_slots[touch_slot_count - 1],
KICK_TOUCH_UP, 0, 0); KICK_TOUCH_UP, 0, 0);

View file

@ -4897,6 +4897,71 @@ def test_overlay_touch_select(session):
assert session.active() == target 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): def test_synth_feed_drives_host_listeners(tmp_path):
"""Synthesized host pointer/touch events go through the real host """Synthesized host pointer/touch events go through the real host
wl_pointer/wl_touch listeners instead of bypassing them for the shared wl_pointer/wl_touch listeners instead of bypassing them for the shared