feat: cycle overlay tabs with the mouse wheel

Rolling the wheel while the overlay is up now steps through tabs
exactly like Alt+Up/Down: wheel down goes to the next tab, wheel up
to the previous one, wrapping at the ends, previewed live and
committed when Alt is released. Notched wheels step one tab per tick;
smooth touchpad-style scroll accumulates to 15px per step. Horizontal
scroll still goes to clients, and with the overlay down nothing
changes.

Swallowed vertical scroll never reaches clients (both the nested and
standalone axis paths share one overlay_handle_axis gate); wltest
grows axis_source/stop/discrete handlers so forwarded scroll no
longer aborts test clients, plus axis lines in --pointer-out.
This commit is contained in:
Andrew Hamilton 2026-09-17 12:33:00 +10:00
parent a4e5633a27
commit 0b9012cd6f
3 changed files with 175 additions and 3 deletions

63
kick.c
View file

@ -1057,6 +1057,10 @@ static bool overlay_hit_test(double ox, double oy,
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);
/* Vertical wheel while the overlay eats pointer input: step tabs like
Alt+Up/Down instead of scrolling the client. Returns true when eaten
(callers send frame and return). Defined with overlay_eats_pointer. */
static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete);
/* 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
@ -9304,6 +9308,11 @@ static void handle_window_meta(struct wl_listener *l, void *data) {
}
}
/* Smooth-scroll accumulation (output pixels) towards one overlay tab step.
Carries the remainder across events; cleared whenever the overlay hides
so no stale fraction leaks into the next session. */
static double overlay_wheel_acc = 0.0;
static void update_overlay_visible(bool alt) {
if (alt == host.overlay_visible) return;
if (alt) {
@ -9326,6 +9335,7 @@ static void update_overlay_visible(bool alt) {
}
server.mru_alt_anchor = NULL;
server.mru_alt_navigated = false;
overlay_wheel_acc = 0.0;
}
host.overlay_visible = alt;
/* The overlay draws as a second pass on top of the composited frame, so
@ -10684,6 +10694,8 @@ static void standalone_ptr_axis_event(struct wl_listener *listener, void *data)
(void)listener;
struct wlr_pointer_axis_event *ev = data;
if (!server.seat) return;
if (overlay_handle_axis(ev->orientation, ev->delta, ev->delta_discrete))
return;
wlr_seat_pointer_notify_axis(server.seat, ev->time_msec, ev->orientation,
ev->delta, ev->delta_discrete, ev->source,
WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
@ -11161,6 +11173,55 @@ static bool overlay_eats_pointer(void) {
!server.session_locked;
}
/* Smooth-scroll pixels per overlay tab step. Notched wheels send discrete
ticks (one step each) alongside their pixel value; smooth-only devices
send value alone, which accumulates here. */
#define OVERLAY_WHEEL_STEP 15.0
/* Whether the wheel has anywhere to go: two or more live, non-closing tabs
(the same predicate cycle_tab steps over). With fewer, wheel input is
still swallowed but steps and logs nothing. */
static bool overlay_can_cycle(void) {
int n = 0;
struct kick_tab *t;
wl_list_for_each(t, &server.tabs, link)
if (tab_live(t) && !tab_closing(t) && ++n >= 2)
return true;
return false;
}
/* Vertical wheel while the overlay eats pointer input: cycle tabs exactly
like Alt+Up/Down (preview now, Alt release commits via the usual overlay
path). discrete > 0 (wheel down) steps to the next tab, < 0 to the
previous one; a zero discrete accumulates value towards one step.
Returns true when eaten (the axis must not reach the client). */
static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete) {
if (axis != 0 || !overlay_eats_pointer())
return false;
if (server.seat)
wlr_seat_pointer_notify_frame(server.seat);
if (!overlay_can_cycle())
return true;
if (discrete != 0) {
int dir = discrete > 0 ? 1 : -1;
int32_t n = discrete > 0 ? discrete : -discrete;
for (int32_t i = 0; i < n; i++) {
cycle_tab(dir);
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
}
return true;
}
overlay_wheel_acc += value;
while (overlay_wheel_acc >= OVERLAY_WHEEL_STEP ||
overlay_wheel_acc <= -OVERLAY_WHEEL_STEP) {
int dir = overlay_wheel_acc > 0 ? 1 : -1;
cycle_tab(dir);
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
overlay_wheel_acc -= dir * OVERLAY_WHEEL_STEP;
}
return true;
}
static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action,
double ox, double oy) {
if (!server.seat) return;
@ -11346,6 +11407,8 @@ static void host_ptr_axis(void *data, struct wl_pointer *p, uint32_t time,
if (!server.seat) return;
int32_t discrete = axis < 2 ? host.axis_discrete[axis] : 0;
host.axis_discrete[axis] = 0;
if (overlay_handle_axis(axis, wl_fixed_to_double(value), discrete))
return;
wlr_seat_pointer_notify_axis(server.seat, time, axis,
wl_fixed_to_double(value), discrete,
host.axis_source,