use super::*;
const GESTURE: &[u16] = &[reprog_controls::GESTURE_BUTTON_CID];
#[test]
fn alternate_gesture_control_delivers_click_and_swipe() {
for swipe in [false, true] {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let sources = &[0x00d0];
handle_reprog(
&mut acc,
RawControlEvent::DivertedButtons([0x00d0, 0, 0, 0]),
sources,
&[],
&[],
&tx,
);
if swipe {
acc.backdate_hold_for_test();
stale_report(&mut acc, &tx);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
sources,
&[],
&[],
&tx,
);
}
handle_reprog(&mut acc, release(), sources, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
if swipe {
GestureDirection::Right
} else {
GestureDirection::Click
}
))
);
assert!(
next_gesture(&mut rx).is_err(),
"one hold must dispatch only once"
);
}
}
const PANEL: &[u16] = &[reprog_controls::HAPTIC_PANEL_CID];
const BOTH: &[u16] = &[
reprog_controls::GESTURE_BUTTON_CID,
reprog_controls::HAPTIC_PANEL_CID,
];
fn stale_report(acc: &mut CaptureAccum, tx: &mpsc::UnboundedSender<CapturedInput>) {
let stale = RawControlEvent::RawXy {
dx: -3000,
dy: 2400,
};
handle_reprog(acc, stale, &[], &[], &[], tx);
}
fn press() -> RawControlEvent {
RawControlEvent::DivertedButtons([reprog_controls::GESTURE_BUTTON_CID, 0, 0, 0])
}
fn panel_press() -> RawControlEvent {
RawControlEvent::DivertedButtons([reprog_controls::HAPTIC_PANEL_CID, 0, 0, 0])
}
fn both_press() -> RawControlEvent {
RawControlEvent::DivertedButtons([
reprog_controls::GESTURE_BUTTON_CID,
reprog_controls::HAPTIC_PANEL_CID,
0,
0,
])
}
fn release() -> RawControlEvent {
RawControlEvent::DivertedButtons([0, 0, 0, 0])
}
fn next_gesture(
rx: &mut mpsc::UnboundedReceiver<CapturedInput>,
) -> Result<CapturedInput, mpsc::error::TryRecvError> {
loop {
let input = rx.try_recv()?;
if matches!(input, CapturedInput::Gesture(..)) {
return Ok(input);
}
}
}
#[test]
fn a_still_held_second_source_takes_over_when_the_holder_releases() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, both_press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, panel_press(), BOTH, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Click
)),
"the released holder still clicks"
);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
BOTH,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::HapticPanel,
GestureDirection::Right
)),
"the taken-over hold dispatches through the panel's own map"
);
handle_reprog(&mut acc, release(), BOTH, &[], &[], &tx);
assert!(
next_gesture(&mut rx).is_err(),
"a committed takeover swipe must not also click on release"
);
}
#[test]
fn raw_xy_during_a_two_source_overlap_is_dropped_not_misattributed() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(&mut acc, both_press(), BOTH, &[], &[], &tx);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
BOTH,
&[],
&[],
&tx,
);
assert!(
next_gesture(&mut rx).is_err(),
"ambiguous overlap motion must not commit a swipe"
);
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
BOTH,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Right
)),
"the original hold resumes once the overlap ends"
);
}
#[test]
fn a_same_report_swap_to_the_panel_still_discards_its_contact_jump() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, panel_press(), BOTH, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Click
)),
"the swapped-out holder still clicks"
);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: -3000, dy: 40 },
BOTH,
&[],
&[],
&tx,
);
assert!(
next_gesture(&mut rx).is_err(),
"the panel's contact jump must not commit a swipe"
);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
BOTH,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::HapticPanel,
GestureDirection::Right
))
);
}
#[test]
fn quick_tap_is_a_click_even_while_the_cursor_moves() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), GESTURE, &[], &[], &tx);
stale_report(&mut acc, &tx);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
GESTURE,
&[],
&[],
&tx,
);
handle_reprog(&mut acc, release(), GESTURE, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Click
))
);
assert!(
next_gesture(&mut rx).is_err(),
"a quick tap emits exactly one click"
);
}
#[test]
fn a_held_gesture_commits_a_swipe_and_does_not_also_click() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), GESTURE, &[], &[], &tx);
stale_report(&mut acc, &tx);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
GESTURE,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Right
))
);
handle_reprog(&mut acc, release(), GESTURE, &[], &[], &tx);
assert!(
next_gesture(&mut rx).is_err(),
"a committed swipe must not also click on release"
);
}
#[test]
fn the_haptic_panel_gestures_when_diverted_for_gestures() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, panel_press(), PANEL, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: -3000, dy: 40 },
PANEL,
&[],
&[],
&tx,
);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 5, dy: -120 },
PANEL,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::HapticPanel,
GestureDirection::Up
))
);
handle_reprog(&mut acc, release(), PANEL, &[], &[], &tx);
assert!(
next_gesture(&mut rx).is_err(),
"a committed panel swipe must not also click on release"
);
}
#[test]
fn a_quick_panel_tap_is_a_click() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, panel_press(), PANEL, &[], &[], &tx);
handle_reprog(&mut acc, release(), PANEL, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::HapticPanel,
GestureDirection::Click
))
);
assert!(
next_gesture(&mut rx).is_err(),
"a panel tap emits exactly one click"
);
}
#[test]
fn the_panels_first_raw_xy_sample_after_contact_is_discarded() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, panel_press(), PANEL, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: -3000, dy: 40 },
PANEL,
&[],
&[],
&tx,
);
assert!(
next_gesture(&mut rx).is_err(),
"the contact jump must not commit a swipe"
);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
PANEL,
&[],
&[],
&tx,
);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::HapticPanel,
GestureDirection::Right
))
);
}
#[test]
fn the_dedicated_buttons_first_sample_is_discarded_as_stale() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let xy = |dx, dy| RawControlEvent::RawXy { dx, dy };
handle_reprog(&mut acc, press(), GESTURE, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(&mut acc, xy(-2717, -105), GESTURE, &[], &[], &tx);
assert!(
next_gesture(&mut rx).is_err(),
"travel from before the press must not commit a direction"
);
for dx in [115, 49, 76, 66] {
handle_reprog(&mut acc, xy(dx, 2), GESTURE, &[], &[], &tx);
}
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Right
)),
"the stroke made during the hold decides"
);
handle_reprog(&mut acc, release(), GESTURE, &[], &[], &tx);
handle_reprog(&mut acc, press(), GESTURE, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(&mut acc, xy(633, -1234), GESTURE, &[], &[], &tx);
handle_reprog(&mut acc, release(), GESTURE, &[], &[], &tx);
assert_eq!(
next_gesture(&mut rx),
Ok(CapturedInput::Gesture(
ButtonId::GestureButton,
GestureDirection::Click
)),
"a hold with only its stale report is a click"
);
}
#[test]
fn an_undiverted_gesture_source_does_not_gesture() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), PANEL, &[], &[], &tx);
acc.backdate_hold_for_test();
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
PANEL,
&[],
&[],
&tx,
);
handle_reprog(&mut acc, release(), PANEL, &[], &[], &tx);
assert!(
next_gesture(&mut rx).is_err(),
"a non-owner source must neither gesture nor click"
);
}
#[test]
fn gesture_sources_emit_independent_edges_without_snapshot_duplicates() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, both_press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, panel_press(), BOTH, &[], &[], &tx);
handle_reprog(&mut acc, release(), BOTH, &[], &[], &tx);
let edges: Vec<_> = std::iter::from_fn(|| rx.try_recv().ok())
.filter(|input| !matches!(input, CapturedInput::Gesture(..)))
.collect();
assert_eq!(
edges,
vec![
CapturedInput::ButtonDown(ButtonId::GestureButton),
CapturedInput::ButtonDown(ButtonId::HapticPanel),
CapturedInput::ButtonUp(ButtonId::GestureButton),
CapturedInput::ButtonUp(ButtonId::HapticPanel),
]
);
}
#[test]
fn a_plain_diverted_gesture_button_presses_without_gesturing() {
for cid in [0x00c3, 0x00d0] {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let buttons = [(cid, ButtonId::GestureButton)];
handle_reprog(
&mut acc,
RawControlEvent::DivertedButtons([cid, 0, 0, 0]),
&[],
&[],
&buttons,
&tx,
);
handle_reprog(
&mut acc,
RawControlEvent::RawXy { dx: 120, dy: 5 },
&[],
&[],
&buttons,
&tx,
);
handle_reprog(&mut acc, release(), &[], &[], &buttons, &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::GestureButton))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::GestureButton))
);
assert!(
rx.try_recv().is_err(),
"a plain-diverted gesture button must not also emit a gesture click"
);
}
}
#[test]
fn a_plain_diverted_haptic_panel_presses_as_its_own_button() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let buttons = [(reprog_controls::HAPTIC_PANEL_CID, ButtonId::HapticPanel)];
handle_reprog(&mut acc, panel_press(), &[], &[], &buttons, &tx);
handle_reprog(&mut acc, release(), &[], &[], &buttons, &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::HapticPanel))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::HapticPanel))
);
assert!(
rx.try_recv().is_err(),
"a plain-diverted panel must not also emit a gesture click"
);
}
#[test]
fn plain_button_snapshots_emit_each_edge_once_and_keep_buttons_independent() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let buttons = [(0x0053, ButtonId::Back), (0x0056, ButtonId::Forward)];
let back = RawControlEvent::DivertedButtons([0x0053, 0, 0, 0]);
let both = RawControlEvent::DivertedButtons([0x0053, 0x0056, 0, 0]);
let forward = RawControlEvent::DivertedButtons([0x0056, 0, 0, 0]);
handle_reprog(&mut acc, back, &[], &[], &buttons, &tx);
handle_reprog(&mut acc, back, &[], &[], &buttons, &tx);
handle_reprog(&mut acc, both, &[], &[], &buttons, &tx);
handle_reprog(&mut acc, forward, &[], &[], &buttons, &tx);
handle_reprog(&mut acc, release(), &[], &[], &buttons, &tx);
assert_eq!(rx.try_recv(), Ok(CapturedInput::ButtonDown(ButtonId::Back)));
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::Forward))
);
assert_eq!(rx.try_recv(), Ok(CapturedInput::ButtonUp(ButtonId::Back)));
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::Forward))
);
assert!(rx.try_recv().is_err(), "unchanged snapshots emit no edges");
}
#[test]
fn a_side_gesture_button_uses_its_hidpp_raw_xy() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let cid = 0x0056;
let buttons = [(cid, ButtonId::Forward)];
let down = RawControlEvent::DivertedButtons([cid, 0, 0, 0]);
acc.on_event(down, &[], &[], &buttons, &[], &tx);
stale_report(&mut acc, &tx);
acc.backdate_hold_for_test();
acc.on_event(
RawControlEvent::RawXy { dx: -120, dy: 5 },
&[],
&[],
&buttons,
&[],
&tx,
);
acc.on_event(release(), &[], &[], &buttons, &[], &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::Forward))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::Gesture(
ButtonId::Forward,
GestureDirection::Left
))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::Forward))
);
assert!(
rx.try_recv().is_err(),
"a committed side-button swipe must not also click on release"
);
}
#[test]
fn a_side_gesture_button_tap_is_a_click() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let cid = 0x0056;
let buttons = [(cid, ButtonId::Forward)];
let down = RawControlEvent::DivertedButtons([cid, 0, 0, 0]);
acc.on_event(down, &[], &[], &buttons, &[], &tx);
acc.on_event(release(), &[], &[], &buttons, &[], &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::Forward))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::Gesture(
ButtonId::Forward,
GestureDirection::Click
))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::Forward))
);
assert_eq!(rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
}
#[test]
fn a_dpi_gesture_button_uses_the_shared_raw_xy_path() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let cid = reprog_controls::DPI_MODE_SHIFT_CIDS[0];
let buttons = [(cid, ButtonId::DpiToggle)];
let down = RawControlEvent::DivertedButtons([cid, 0, 0, 0]);
acc.on_event(down, &[], &[], &buttons, &[], &tx);
stale_report(&mut acc, &tx);
acc.backdate_hold_for_test();
acc.on_event(
RawControlEvent::RawXy { dx: 5, dy: -120 },
&[],
&[],
&buttons,
&[],
&tx,
);
acc.on_event(release(), &[], &[], &buttons, &[], &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::DpiToggle))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::Gesture(
ButtonId::DpiToggle,
GestureDirection::Up
))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::DpiToggle))
);
assert_eq!(rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
}
#[test]
fn a_held_dpi_button_presses_once_on_the_rising_edge() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let dpi = reprog_controls::DPI_MODE_SHIFT_CIDS[0];
let down = RawControlEvent::DivertedButtons([dpi, 0, 0, 0]);
handle_reprog(&mut acc, down, GESTURE, &[dpi], &[], &tx);
handle_reprog(&mut acc, down, GESTURE, &[dpi], &[], &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::DpiToggle))
);
assert!(rx.try_recv().is_err(), "a held DPI button presses once");
}
#[test]
fn a_dpi_button_re_presses_after_a_release() {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut acc = CaptureAccum::default();
let dpi = reprog_controls::DPI_MODE_SHIFT_CIDS[0];
let down = RawControlEvent::DivertedButtons([dpi, 0, 0, 0]);
let up = RawControlEvent::DivertedButtons([0, 0, 0, 0]);
handle_reprog(&mut acc, down, GESTURE, &[dpi], &[], &tx);
handle_reprog(&mut acc, up, GESTURE, &[dpi], &[], &tx);
handle_reprog(&mut acc, down, GESTURE, &[dpi], &[], &tx);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::DpiToggle))
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonUp(ButtonId::DpiToggle)),
"the falling edge must be preserved"
);
assert_eq!(
rx.try_recv(),
Ok(CapturedInput::ButtonDown(ButtonId::DpiToggle)),
"a release re-arms the rising edge"
);
assert!(
rx.try_recv().is_err(),
"press → release → press emits exactly three lifecycle edges"
);
}