diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 03:15:28 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 6cd33661064df4163f3bd98db432526ad2165b7f (patch) | |
| tree | 6d3cffe1f59eb6b5df8193ddd9c092f781f0979f /src | |
| parent | f2b8a25d0a25df778bce57efa4c2c7282c71893b (diff) | |
| download | pardes-6cd33661064df4163f3bd98db432526ad2165b7f.tar.gz pardes-6cd33661064df4163f3bd98db432526ad2165b7f.zip | |
a mostly-vertical touchpad swipe stays vertical
The pad faithfully turns a two-finger scroll's sideways drift into wheel_left
and wheel_right, so a plain scroll slid the view sideways underneath you. Every
vertical tick now re-arms a guard and every horizontal tick spends one instead
of scrolling, so horizontal has to EARN its way back by landing three ticks in
a row with no vertical among them.
Clock-free on purpose: the core is a state machine with no timestamp on a mouse
event, and faking one by counting renders would be worse than the counter. The
guard is only ever armed BY vertical scrolling, so a horizontal swipe from a
still view still moves on its first tick — only horizontal that interrupts
vertical has anything to prove.
A tilt wheel gets the same treatment, where recent-vertical is a much weaker
signal of accident. Deliberate: the only honest fix is a per-device flag out of
the shell, and that layer costs more than the three clicks it would save.
The rule is one pure function next to the number it reads, with an inline test
written to hold for any tuning of that number. unit-test grew a fourth binary
over the core module hxdiff already links. New golden wheeldrift; none moved.
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.zig | 56 | ||||
| -rw-r--r-- | src/pardes.zig | 18 |
2 files changed, 74 insertions, 0 deletions
diff --git a/src/config.zig b/src/config.zig index 35be4b9f..7f83155a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -303,6 +303,62 @@ pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }}; pub const wheel_rows: i32 = 1; pub const wheel_cols: i32 = 4; +/// Touchpad drift guard, in ticks. A two-finger swipe that is MEANT to be +/// vertical carries a little sideways drift, and the pad faithfully turns that +/// drift into wheel_left/wheel_right — so a plain scroll slides the view +/// sideways under you. Every vertical tick re-arms the guard to this many +/// ticks and every horizontal tick spends one instead of scrolling, which +/// makes horizontal EARN its way back: it has to land this many ticks in a row +/// with no vertical among them. 3, because drift arrives in ones and twos — +/// at 1 or 2 a doubled drift tick mid-swipe still gets through, and much +/// higher starts eating deliberate swipes. Set 0 to disable the heuristic. +/// +/// It costs nothing at rest: the guard is only armed by vertical scrolling, so +/// a horizontal swipe that starts from a still view moves on its FIRST tick. +/// Note this applies to a tilt wheel too, where "recent vertical" is a much +/// weaker signal of accident — a mouse would rather not have it. Living with +/// that is deliberate: the only honest fix is a per-device flag out of the +/// shell (libinput/SDL know which is which, vaxis does not), and paying for a +/// device-detection layer to spare a tilt wheel three clicks after a scroll is +/// a worse trade than the three clicks. +pub const wheel_guard_ticks: u8 = 3; + +/// The whole guard, as one state machine, so it can be tested as one thing: +/// fold a wheel tick into `guard` and answer whether it scrolls. The core owns +/// the counter (Pardes.wheel_guard) because the gesture belongs to the DEVICE, +/// not to whichever pane the pointer happens to sit over. +pub fn wheelTick(guard: *u8, vertical: bool) bool { + if (vertical) { + guard.* = wheel_guard_ticks; + return true; + } + if (guard.* == 0) return true; + guard.* -= 1; + return false; +} + +// written to hold for ANY tuning of wheel_guard_ticks, since tuning it by hand +// is what this file is for — a test that pinned the number 3 would just be a +// second place to edit it +test "wheel drift guard" { + var g: u8 = 0; + // from rest, horizontal moves on the first tick — nothing to prove + try std.testing.expect(wheelTick(&g, false)); + try std.testing.expect(wheelTick(&g, false)); + if (wheel_guard_ticks == 0) return; // guard disabled: nothing left to check + // a vertical swipe with drift mixed in: every sideways tick is swallowed, + // because each vertical tick re-arms the guard in full + for (0..4) |_| try std.testing.expect(wheelTick(&g, true)); + try std.testing.expect(!wheelTick(&g, false)); + try std.testing.expect(wheelTick(&g, true)); + try std.testing.expect(!wheelTick(&g, false)); + // the deliberate horizontal swipe that follows pays off the rest of the + // guard tick by tick, then runs free + for (1..wheel_guard_ticks) |_| try std.testing.expect(!wheelTick(&g, false)); + try std.testing.expect(wheelTick(&g, false)); + try std.testing.expect(wheelTick(&g, false)); +} + // ---- layout numbers that are taste ---- /// the file pane's line-number gutter, in columns diff --git a/src/pardes.zig b/src/pardes.zig index cd01187b..93ea1016 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -993,6 +993,16 @@ pub const Pardes = struct { drag: Drag = .none, hover_col: u16 = 0, hover_row: u16 = 0, + /// touchpad drift guard, counted down in horizontal wheel ticks — see + /// config.wheelTick, which owns the whole rule. Global, and clock-free on + /// purpose: the core has no clock, so "recently scrolled vertically" can + /// only mean "in the last few wheel events", which is all the heuristic + /// needs. + /// ponytail: it therefore never times out — only a horizontal tick spends + /// it, so a sideways swipe an hour after a scroll still pays the toll. If + /// that ever bites, clear it on any non-wheel event: a keypress or a click + /// is proof the gesture ended, and still needs no clock. + wheel_guard: u8 = 0, ctrl_w_pending: bool = false, /// SPC leader in flight, holding the key path typed so far (empty = just /// SPC). Global like ctrl_w_pending — there is one leader and it acts on @@ -4292,11 +4302,19 @@ pub const Pardes = struct { .none => {}, // hover tracked above; motion has no other meaning .wheel_up, .wheel_down => { if (m.kind != .press) return; + // re-arms the drift guard below; vertical always scrolls, so + // the answer is never in doubt here + _ = config.wheelTick(&p.wheel_guard, true); const id = hovered orelse return; p.panes[id].?.scrollBy(if (m.button == .wheel_up) -config.wheel_rows else config.wheel_rows); }, .wheel_left, .wheel_right => { if (m.kind != .press) return; + // a mostly-vertical two-finger swipe's sideways drift dies + // here rather than sliding the view out from under a scroll. + // Charged against the gesture, not the pane, so it runs before + // we ask what is hovered. + if (!config.wheelTick(&p.wheel_guard, false)) return; const id = hovered orelse return; const pane = p.panes[id].?; // ponytail: no right clamp — overscroll shows blank and the |
