feat: steady jittery wheels with KICK_WHEEL_TICKS
A bouncy old wheel emits a burst of discrete ticks per physical notch, and each tick stepped one overlay tab, so the selection jumped around. Discrete ticks now accumulate toward a per-step threshold with net remainders (a +2/-1 burst leaves +1 instead of jumping), mirroring the smooth-scroll accumulator: deliberate flicks still skip multiple tabs. The threshold is KICK_WHEEL_TICKS, read per event like KICK_REPEAT_DELAY_MS; unset or invalid means 1, today's one-notch-per-step, so good hardware sees no change. It only affects overlay cycling, never client scrolling, and the leftover fraction resets when the overlay hides. The smooth 15px threshold is untouched. Covered by test_overlay_wheel_ticks_env, which spawns with KICK_WHEEL_TICKS=3 (via a new _nested_session extra_env knob) and proves two ticks step nowhere while three step exactly once; it fails on unpatched code.
This commit is contained in:
parent
3a6fdd4fd4
commit
01fd76e1ce
2 changed files with 98 additions and 8 deletions
35
kick.c
35
kick.c
|
|
@ -9312,6 +9312,10 @@ static void handle_window_meta(struct wl_listener *l, void *data) {
|
||||||
Carries the remainder across events; cleared whenever the overlay hides
|
Carries the remainder across events; cleared whenever the overlay hides
|
||||||
so no stale fraction leaks into the next session. */
|
so no stale fraction leaks into the next session. */
|
||||||
static double overlay_wheel_acc = 0.0;
|
static double overlay_wheel_acc = 0.0;
|
||||||
|
/* Notched-wheel accumulation (ticks) towards one overlay tab step. A
|
||||||
|
jittery wheel emits a burst of ticks per physical notch; counting several
|
||||||
|
per step stills the selection. Same reset contract as overlay_wheel_acc. */
|
||||||
|
static int overlay_wheel_ticks = 0;
|
||||||
|
|
||||||
static void update_overlay_visible(bool alt) {
|
static void update_overlay_visible(bool alt) {
|
||||||
if (alt == host.overlay_visible) return;
|
if (alt == host.overlay_visible) return;
|
||||||
|
|
@ -9336,6 +9340,7 @@ static void update_overlay_visible(bool alt) {
|
||||||
server.mru_alt_anchor = NULL;
|
server.mru_alt_anchor = NULL;
|
||||||
server.mru_alt_navigated = false;
|
server.mru_alt_navigated = false;
|
||||||
overlay_wheel_acc = 0.0;
|
overlay_wheel_acc = 0.0;
|
||||||
|
overlay_wheel_ticks = 0;
|
||||||
}
|
}
|
||||||
host.overlay_visible = alt;
|
host.overlay_visible = alt;
|
||||||
/* The overlay draws as a second pass on top of the composited frame, so
|
/* The overlay draws as a second pass on top of the composited frame, so
|
||||||
|
|
@ -11181,10 +11186,25 @@ static bool overlay_eats_pointer(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth-scroll pixels per overlay tab step. Notched wheels send discrete
|
/* Smooth-scroll pixels per overlay tab step. Notched wheels send discrete
|
||||||
ticks (one step each) alongside their pixel value; smooth-only devices
|
ticks (accumulated below) alongside their pixel value; smooth-only devices
|
||||||
send value alone, which accumulates here. */
|
send value alone, which accumulates here. */
|
||||||
#define OVERLAY_WHEEL_STEP 15.0
|
#define OVERLAY_WHEEL_STEP 15.0
|
||||||
|
|
||||||
|
/* Wheel ticks (notches) per overlay tab step. Honours KICK_WHEEL_TICKS so a
|
||||||
|
jittery wheel can be steadied (e.g. =3); unset or invalid means 1, today's
|
||||||
|
one-notch-per-step. Read per event like KICK_REPEAT_DELAY_MS. */
|
||||||
|
#define KICK_WHEEL_TICKS 1
|
||||||
|
|
||||||
|
static long kick_wheel_ticks(void) {
|
||||||
|
const char *e = getenv("KICK_WHEEL_TICKS");
|
||||||
|
if (e && *e) {
|
||||||
|
long v = atol(e);
|
||||||
|
if (v > 0)
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return KICK_WHEEL_TICKS;
|
||||||
|
}
|
||||||
|
|
||||||
/* Whether the wheel has anywhere to go: two or more live, non-closing tabs
|
/* 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
|
(the same predicate cycle_tab steps over). With fewer, wheel input is
|
||||||
still swallowed but steps and logs nothing. */
|
still swallowed but steps and logs nothing. */
|
||||||
|
|
@ -11200,8 +11220,9 @@ static bool overlay_can_cycle(void) {
|
||||||
/* Vertical wheel while the overlay eats pointer input: cycle tabs exactly
|
/* Vertical wheel while the overlay eats pointer input: cycle tabs exactly
|
||||||
like Alt+Up/Down (preview now, Alt release commits via the usual overlay
|
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
|
path). discrete > 0 (wheel down) steps to the next tab, < 0 to the
|
||||||
previous one; a zero discrete accumulates value towards one step.
|
previous one, accumulated KICK_WHEEL_TICKS per step; a zero discrete
|
||||||
Returns true when eaten (the axis must not reach the client). */
|
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) {
|
static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete) {
|
||||||
if (axis != 0 || !overlay_eats_pointer())
|
if (axis != 0 || !overlay_eats_pointer())
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -11210,11 +11231,13 @@ static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete) {
|
||||||
if (!overlay_can_cycle())
|
if (!overlay_can_cycle())
|
||||||
return true;
|
return true;
|
||||||
if (discrete != 0) {
|
if (discrete != 0) {
|
||||||
int dir = discrete > 0 ? 1 : -1;
|
overlay_wheel_ticks += discrete;
|
||||||
int32_t n = discrete > 0 ? discrete : -discrete;
|
long per = kick_wheel_ticks();
|
||||||
for (int32_t i = 0; i < n; i++) {
|
while (overlay_wheel_ticks >= per || overlay_wheel_ticks <= -per) {
|
||||||
|
int dir = overlay_wheel_ticks > 0 ? 1 : -1;
|
||||||
cycle_tab(dir);
|
cycle_tab(dir);
|
||||||
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
|
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
|
||||||
|
overlay_wheel_ticks -= dir * (int)per;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3265,7 +3265,7 @@ def test_tab_death_activates_left_neighbor(session):
|
||||||
_assert_alive(session, "compositor exited after first tab died")
|
_assert_alive(session, "compositor exited after first tab died")
|
||||||
|
|
||||||
|
|
||||||
def _nested_session(*tabs, extra_args=(), repeat_off=True):
|
def _nested_session(*tabs, extra_args=(), repeat_off=True, extra_env=()):
|
||||||
"""Launch a nested compositor with an arbitrary tab list (each tab is a
|
"""Launch a nested compositor with an arbitrary tab list (each tab is a
|
||||||
shell command string, as on the CLI). extra_args are passed on the CLI
|
shell command string, as on the CLI). extra_args are passed on the CLI
|
||||||
before the tabs (e.g. --modifier super). The `session` fixture exists for
|
before the tabs (e.g. --modifier super). The `session` fixture exists for
|
||||||
|
|
@ -3273,6 +3273,8 @@ def _nested_session(*tabs, extra_args=(), repeat_off=True):
|
||||||
repeat_off stretches the keybind repeat delay past any load-stalled tap
|
repeat_off stretches the keybind repeat delay past any load-stalled tap
|
||||||
so discrete chords never catch a wall-clock repeat (see KICK_REPEAT_OFF);
|
so discrete chords never catch a wall-clock repeat (see KICK_REPEAT_OFF);
|
||||||
the hold-to-repeat test opts out with repeat_off=False.
|
the hold-to-repeat test opts out with repeat_off=False.
|
||||||
|
extra_env appends VAR=value strings to the spawn environment (e.g.
|
||||||
|
tuning knobs like KICK_WHEEL_TICKS).
|
||||||
A spawn whose client dies before opening any window (load-starved client
|
A spawn whose client dies before opening any window (load-starved client
|
||||||
startup: "command exited without opening a window" + "no tabs left") is
|
startup: "command exited without opening a window" + "no tabs left") is
|
||||||
retried up to 3 times -- no test asserts boot failure, so this masks no
|
retried up to 3 times -- no test asserts boot failure, so this masks no
|
||||||
|
|
@ -3290,7 +3292,8 @@ def _nested_session(*tabs, extra_args=(), repeat_off=True):
|
||||||
"KICK_LOG=2", "KICK_SYNTH_INPUT=1",
|
"KICK_LOG=2", "KICK_SYNTH_INPUT=1",
|
||||||
"KICK_DUMP_DIR=" + dump_dir,
|
"KICK_DUMP_DIR=" + dump_dir,
|
||||||
"KICK_REPEAT_DELAY_MS=" + (KICK_REPEAT_TEST_DELAY if repeat_off
|
"KICK_REPEAT_DELAY_MS=" + (KICK_REPEAT_TEST_DELAY if repeat_off
|
||||||
else KICK_REPEAT_DEFAULT)]
|
else KICK_REPEAT_DEFAULT),
|
||||||
|
*extra_env]
|
||||||
if os.environ.get("KICK_LIBDECOR"):
|
if os.environ.get("KICK_LIBDECOR"):
|
||||||
cmd.append("KICK_LIBDECOR=" + os.environ["KICK_LIBDECOR"])
|
cmd.append("KICK_LIBDECOR=" + os.environ["KICK_LIBDECOR"])
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
|
|
@ -5099,6 +5102,70 @@ def test_overlay_wheel_cycle(session):
|
||||||
assert session.active() == other
|
assert session.active() == other
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_wheel_ticks_env():
|
||||||
|
"""KICK_WHEEL_TICKS=N steadies a jittery wheel: with =3, two ticks step
|
||||||
|
nowhere, the third steps exactly one tab, and six ticks step two tabs.
|
||||||
|
Unset keeps today's one-notch-per-step (see test_overlay_wheel_cycle).
|
||||||
|
Spawns its own session since the shared fixture has a fixed env."""
|
||||||
|
s = _nested_session(f"{WL} '#ff0000'", f"{WL} '#0000ff'",
|
||||||
|
extra_env=("KICK_WHEEL_TICKS=3",))
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_surface_mapped(0)
|
||||||
|
s.wait_surface_mapped(1)
|
||||||
|
start = s.active()
|
||||||
|
other = 0 if start != 0 else 1
|
||||||
|
|
||||||
|
def tick(n=1, sign=1):
|
||||||
|
for _ in range(n):
|
||||||
|
s.host_ptr(HP_DISCRETE, 0, 1 if sign > 0 else 0xFFFFFFFF)
|
||||||
|
s.host_ptr(HP_AXIS, 0, 15 if sign > 0 else 0x100000000 - 15)
|
||||||
|
time.sleep(0.08)
|
||||||
|
|
||||||
|
def wheel_count():
|
||||||
|
return len(re.findall(r"\[wheel\] cycle", s.read_log()))
|
||||||
|
|
||||||
|
s.mods(ALT)
|
||||||
|
try:
|
||||||
|
time.sleep(0.5)
|
||||||
|
# Two ticks of three: no step, still on the start tab.
|
||||||
|
n0 = wheel_count()
|
||||||
|
tick(2)
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert wheel_count() == n0, "sub-threshold ticks stepped a tab"
|
||||||
|
assert s.active() == start
|
||||||
|
# Third tick: exactly one step.
|
||||||
|
tick(1)
|
||||||
|
deadline = time.monotonic() + 10.0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if wheel_count() > n0:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
pytest.fail("three ticks never stepped a tab")
|
||||||
|
assert wheel_count() == n0 + 1, "three ticks stepped more than once"
|
||||||
|
s.wait_active(other, timeout=10.0)
|
||||||
|
# Three more ticks: exactly one more step (back to start).
|
||||||
|
n1 = wheel_count()
|
||||||
|
tick(3)
|
||||||
|
deadline = time.monotonic() + 10.0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if wheel_count() > n1:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
pytest.fail("three more ticks never stepped again")
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert wheel_count() == n1 + 1, "three ticks stepped more than once"
|
||||||
|
s.wait_active(start, timeout=10.0)
|
||||||
|
finally:
|
||||||
|
s.mods(0)
|
||||||
|
s.wait_for(r"\[mru\] commit app=%d" % start, timeout=10.0)
|
||||||
|
assert s.active() == start
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
def test_overlay_wheel_swallowed_by_client(tmp_path):
|
def test_overlay_wheel_swallowed_by_client(tmp_path):
|
||||||
"""While the overlay is up, vertical scroll never reaches clients but
|
"""While the overlay is up, vertical scroll never reaches clients but
|
||||||
horizontal scroll still does: a --pointer-out client sees no new axis
|
horizontal scroll still does: a --pointer-out client sees no new axis
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue