Conversation
VolumeUp/VolumeDown were dispatched through the same discrete-action state machine as any other thumb-wheel binding: one threshold crossing fires the action once, then a 200ms cooldown discards every further increment until it expires. Sensitivity only changed how much rotation was needed to cross the threshold the first time, so a single swipe — however far or fast, and regardless of sensitivity — could never move the system volume by more than one native step, since the cooldown threw away the rest of the swipe's distance instead of queuing it. Volume behaves like a physical volume wheel, not a one-shot action like MissionControl (where firing more than once per flick would be wrong): let it fire once per threshold crossed within a single swipe, scaled by sensitivity, with no cooldown gate between repeats. Fixes AprilNEA#887
|
| for _ in 0..repeats { | ||
| self.outputs.actions.dispatch(action, Some(key)); | ||
| } |
There was a problem hiding this comment.
Unbounded synchronous dispatch burst
At maximum sensitivity, the threshold is 1, while one captured i16 wheel increment can have a magnitude of 32,768. This can produce FireAction(32768), causing this loop to synchronously send 32,768 native volume actions. An unusually large device event could therefore tie up the input callback and flood the OS with volume events. Please cap or otherwise bound the repeats accepted from one event.
There was a problem hiding this comment.
Good catch, valid — fixed in ab12fc8. Added a MAX_REPEATS_PER_EVENT cap (20) so one event's repeats are bounded regardless of magnitude; a real swipe never legitimately needs more than a handful of fires, and anything beyond the cap just carries the leftover progress into the next event instead of dropping it. Added a test proving a single event at i16::MAX magnitude and max sensitivity (threshold 1) still only fires the capped count.
A captured rotation increment is a signed i16; at max sensitivity (threshold == 1) an unusually large or corrupt report could otherwise claim a magnitude of thousands, synchronously dispatching that many native volume presses from one input callback. Bound it to a repeat count no legitimate swipe ever needs.
| let repeats = (increments / threshold).min(MAX_REPEATS_PER_EVENT); | ||
| increments -= repeats * threshold; |
There was a problem hiding this comment.
The per-event cap retains every excess threshold in increments. A large report at threshold 1 fires 20 actions but leaves the remaining distance in persistent state, so each following wheel event within the 300 ms decay window can fire another 20 actions from that stale distance. This spreads the volume flood across callbacks instead of discarding the pathological excess; clamp or discard the excess accumulator when the cap is reached.
Summary
VolumeUp/VolumeDownbindings went through the samediscrete-action state machine as any other one-shot binding
(
MissionControl,NextTab, …): one threshold crossing fires the actiononce, then a 200ms cooldown discards every further rotation increment
until it expires. Sensitivity only changed how much rotation was needed to
cross the threshold the first time — it never let a single swipe fire
more than once — so no matter how high the sensitivity, one swipe could
never move the system volume by more than one native OS step (reported as
"the volume only goes up or down by 2").
action: a longer or faster swipe should be able to fire several times,
scaled by sensitivity. The cooldown exists specifically to stop a one-shot
action from repeating on a fast spin (firing
MissionControlfive timesfrom one flick would be wrong) — that protection doesn't apply to Volume.
Changes
crates/openlogi-agent-core/src/watchers/gesture/dispatch/wheel.rs:WheelOutput::FireActionnow carries a repeat count (u32).advance_actionclassifiesVolumeUp/VolumeDownas repeatable(
is_repeatable) and, for those, computes how many complete sensitivitythresholds the accumulated swipe distance spans and fires that many times
in one update, with no cooldown gate between them; every other discrete
action keeps the exact previous cooldown-gated, fire-once behavior.
crates/openlogi-agent-core/src/watchers/gesture/dispatch.rs: theFireActiondispatch site now issues the actionrepeatstimes.Testing
cargo test -p openlogi-agent-core -p openlogi-agent(256 tests) —updated existing
FireActionassertions to the newFireAction(1)shape,moved the cooldown/decay/binding-change tests that used
VolumeUppurelyas a stand-in discrete action onto a genuinely non-repeatable action
(
NextTab/PrevTab) so they keep testing cooldown semantics correctly,and added
a_repeatable_action_fires_once_per_threshold_crossed_in_one_swipeand
repeatable_action_progress_below_threshold_carries_overproving thefix: a swipe spanning three thresholds fires three times in one update,
and a repeatable action is never cooldown-gated the way an ordinary one is.
cargo fmt --all -- --checkRUSTFLAGS="-D warnings" cargo clippy -p openlogi-agent-core -p openlogi-agent --all-targets -- -D warnings(affected-package tier: this change is confined to
openlogi-agent-core,whose only reverse dependency is
openlogi-agent; noCargo.toml/lock,wire-format, or other workspace-wide input changed)
against the exact accumulator/threshold model the real capture session
drives; a maintainer with a thumb-wheel device can confirm a firm swipe on
a Volume binding now changes the system volume by more than one step.
Fixes #887