use super::*;
#[rustfmt::skip]
const GOLDEN_PEN_DOWN: [u8; 120] = [
0x03, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00, 0x16, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x07, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00,
0x70, 0xc6, 0x00, 0x00, 0xcf, 0x37, 0x00, 0x00,
0x80, 0x07, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00,
0x70, 0xc6, 0x00, 0x00, 0xcf, 0x37, 0x00, 0x00,
0x87, 0xd6, 0x12, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x33, 0x1c, 0x5d, 0x04, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00,
0xdb, 0xff, 0xff, 0xff, 0x3c, 0x00, 0x00, 0x00,
];
const ALL_AXES: u32 = PEN_MASK_PRESSURE | PEN_MASK_ROTATION | PEN_MASK_TILT_X | PEN_MASK_TILT_Y;
#[allow(clippy::too_many_arguments)]
fn pen_bytes(
flags: u32,
pen_flags: u32,
pen_mask: u32,
pressure: u32,
rotation: u32,
tilt_x: i32,
tilt_y: i32,
screen: (i32, i32),
) -> [u8; layout::PEN_INFO_SIZE] {
let mut bytes = [0u8; layout::PEN_INFO_SIZE];
let mut put = |offset: usize, value: u32| {
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
};
put(layout::POINTER_TYPE, PT_PEN);
put(layout::POINTER_ID, 0x2A);
put(layout::POINTER_FLAGS, flags);
put(layout::PIXEL_X, screen.0 as u32);
put(layout::PIXEL_Y, screen.1 as u32);
put(layout::TIME, 0x0012_D687);
put(layout::PEN_FLAGS, pen_flags);
put(layout::PEN_MASK, pen_mask);
put(layout::PEN_PRESSURE, pressure);
put(layout::PEN_ROTATION, rotation);
put(layout::PEN_TILT_X, tilt_x as u32);
put(layout::PEN_TILT_Y, tilt_y as u32);
bytes
}
fn history_entry(time_ms: u32, pressure: u32, screen: (i32, i32)) -> [u8; layout::PEN_INFO_SIZE] {
let mut bytes = pen_bytes(
POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT,
0,
ALL_AXES,
pressure,
0,
0,
0,
screen,
);
bytes[layout::TIME..layout::TIME + 4].copy_from_slice(&time_ms.to_le_bytes());
bytes
}
fn history_buffer(newest_first: &[[u8; layout::PEN_INFO_SIZE]]) -> Vec<u8> {
newest_first.iter().flatten().copied().collect()
}
fn touch_bytes(
pointer_id: u32,
flags: u32,
touch_mask: u32,
contact: (i32, i32, i32, i32),
pressure: u32,
) -> [u8; layout::TOUCH_INFO_SIZE] {
let mut bytes = [0u8; layout::TOUCH_INFO_SIZE];
let mut put = |offset: usize, value: u32| {
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
};
put(layout::POINTER_TYPE, PT_TOUCH);
put(layout::POINTER_ID, pointer_id);
put(layout::POINTER_FLAGS, flags);
put(layout::PIXEL_X, contact.0 as u32);
put(layout::PIXEL_Y, contact.1 as u32);
put(layout::TOUCH_MASK, touch_mask);
put(layout::TOUCH_CONTACT_LEFT, contact.0 as u32);
put(layout::TOUCH_CONTACT_TOP, contact.1 as u32);
put(layout::TOUCH_CONTACT_RIGHT, contact.2 as u32);
put(layout::TOUCH_CONTACT_BOTTOM, contact.3 as u32);
put(layout::TOUCH_ORIENTATION, 90);
put(layout::TOUCH_PRESSURE, pressure);
bytes
}
#[test]
fn the_golden_vector_decodes_field_for_field() {
let info = decode_pen_info(&GOLDEN_PEN_DOWN).expect("120 bytes is a whole POINTER_PEN_INFO");
assert_eq!(info.pointer_type, PT_PEN);
assert_eq!(info.pointer_id, 0x2A);
assert_eq!(info.flags, 0x2016);
assert_eq!(info.screen, (1920, 540));
assert_eq!(info.time_ms, 0x0012_D687);
assert_eq!(info.history_count, 1, "an uncoalesced message");
assert_eq!(info.pen_flags, PEN_FLAG_BARREL);
assert_eq!(info.pen_mask, ALL_AXES);
assert_eq!(info.pressure, 512);
assert_eq!(info.rotation, 271);
assert_eq!(info.tilt_x, -37);
assert_eq!(info.tilt_y, 60);
assert!(info.in_proximity() && info.down() && !info.cancelled());
}
#[test]
fn a_short_slice_decodes_to_nothing() {
assert!(decode_pen_info(&GOLDEN_PEN_DOWN[..119]).is_none());
assert!(decode_pen_info(&[]).is_none());
assert!(decode_touch_info(&[0u8; layout::TOUCH_INFO_SIZE - 1]).is_none());
}
#[test]
fn pressure_spans_zero_to_one_over_the_documented_range() {
for (raw, expected) in [(0u32, 0.0f32), (512, 0.5), (1024, 1.0)] {
let bytes = pen_bytes(
POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT,
0,
ALL_AXES,
raw,
0,
0,
0,
(0, 0),
);
let info = decode_pen_info(&bytes).unwrap();
assert_eq!(
info.pressure_normalised(),
Some(expected),
"raw pressure {raw} must normalise to {expected}"
);
}
}
#[test]
fn an_unmeasured_axis_is_none_not_zero() {
let bytes = pen_bytes(
POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT,
0,
0,
0,
0,
0,
0,
(0, 0),
);
let info = decode_pen_info(&bytes).unwrap();
assert_eq!(info.pressure_normalised(), None);
assert_eq!(info.tilt_degrees(), None);
assert_eq!(info.twist_degrees(), None);
let packet = info.to_packet((0, 0), 1.0);
assert_eq!(packet.pressure, 1.0);
assert!(packet.down);
}
#[test]
fn half_a_tilt_pair_is_no_tilt() {
let bytes = pen_bytes(
POINTER_FLAG_INRANGE,
0,
PEN_MASK_TILT_X,
0,
0,
45,
0,
(0, 0),
);
assert_eq!(decode_pen_info(&bytes).unwrap().tilt_degrees(), None);
}
#[test]
fn tilt_survives_the_extremes_of_its_range() {
for (x, y) in [(-90i32, 90i32), (90, -90), (0, 0), (-37, 60)] {
let bytes = pen_bytes(POINTER_FLAG_INRANGE, 0, ALL_AXES, 0, 0, x, y, (0, 0));
let info = decode_pen_info(&bytes).unwrap();
assert_eq!(
info.tilt_degrees(),
Some((x as f32, y as f32)),
"tilt ({x}, {y}) must survive the round trip, sign and all"
);
}
}
#[test]
fn a_tilt_outside_the_documented_range_is_clamped() {
let bytes = pen_bytes(POINTER_FLAG_INRANGE, 0, ALL_AXES, 0, 0, -1000, 1000, (0, 0));
assert_eq!(
decode_pen_info(&bytes).unwrap().tilt_degrees(),
Some((-90.0, 90.0))
);
}
#[test]
fn rotation_becomes_twist() {
for raw in [0u32, 90, 271, 359] {
let bytes = pen_bytes(POINTER_FLAG_INRANGE, 0, ALL_AXES, 0, raw, 0, 0, (0, 0));
let info = decode_pen_info(&bytes).unwrap();
assert_eq!(info.twist_degrees(), Some(raw as f32));
assert_eq!(info.to_packet((0, 0), 1.0).twist, Some(raw as f32));
}
}
#[test]
fn the_eraser_is_a_tool_not_a_button() {
let inverted = pen_bytes(
POINTER_FLAG_INRANGE,
PEN_FLAG_INVERTED,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
);
assert_eq!(decode_pen_info(&inverted).unwrap().tool(), PenKind::Eraser);
let eraser_button = pen_bytes(
POINTER_FLAG_INRANGE,
PEN_FLAG_ERASER,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
);
let info = decode_pen_info(&eraser_button).unwrap();
assert_eq!(info.tool(), PenKind::Eraser);
assert!(info.buttons().contains(PenButtons::ERASER));
assert!(!info.buttons().contains(PenButtons::BARREL));
let plain = pen_bytes(POINTER_FLAG_INRANGE, 0, ALL_AXES, 0, 0, 0, 0, (0, 0));
assert_eq!(decode_pen_info(&plain).unwrap().tool(), PenKind::Pen);
}
#[test]
fn the_barrel_is_read_from_either_place_the_os_reports_it() {
let via_pen_flag = pen_bytes(
POINTER_FLAG_INRANGE,
PEN_FLAG_BARREL,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
);
assert!(
decode_pen_info(&via_pen_flag)
.unwrap()
.buttons()
.contains(PenButtons::BARREL)
);
let via_pointer_flag = pen_bytes(
POINTER_FLAG_INRANGE | POINTER_FLAG_SECONDBUTTON,
0,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
);
assert!(
decode_pen_info(&via_pointer_flag)
.unwrap()
.buttons()
.contains(PenButtons::BARREL)
);
let third = pen_bytes(
POINTER_FLAG_INRANGE | POINTER_FLAG_THIRDBUTTON,
0,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
);
let buttons = decode_pen_info(&third).unwrap().buttons();
assert!(buttons.contains(PenButtons::SECONDARY_BARREL));
assert!(!buttons.contains(PenButtons::BARREL));
}
#[test]
fn proximity_and_contact_are_independent_flags() {
let hovering = decode_pen_info(&pen_bytes(
POINTER_FLAG_INRANGE,
0,
ALL_AXES,
0,
0,
0,
0,
(0, 0),
))
.unwrap();
assert!(hovering.in_proximity() && !hovering.down());
let packet = hovering.to_packet((0, 0), 1.0);
assert!(packet.in_proximity && !packet.down);
assert_eq!(packet.pressure, 0.0, "a hovering pen presses on nothing");
let gone = decode_pen_info(&pen_bytes(0, 0, ALL_AXES, 0, 0, 0, 0, (0, 0))).unwrap();
assert!(!gone.in_proximity() && !gone.down());
}
#[test]
fn screen_pixels_become_window_logical_points() {
let info = decode_pen_info(&GOLDEN_PEN_DOWN).unwrap();
let packet = info.to_packet((100, 80), 2.0);
assert_eq!(
packet.position,
Point::new((1920 - 100) as f32 / 2.0, 230.0)
);
assert_eq!(packet.pressure, 0.5);
assert_eq!(packet.tilt, Some((-37.0, 60.0)));
assert_eq!(packet.twist, Some(271.0));
assert!(packet.buttons.contains(PenButtons::BARREL));
assert!(packet.down && packet.in_proximity);
assert_eq!(packet.tool, PenKind::Pen);
}
#[test]
fn a_nonsense_scale_does_not_produce_an_infinity() {
let info = decode_pen_info(&GOLDEN_PEN_DOWN).unwrap();
let packet = info.to_packet((0, 0), 0.0);
assert!(packet.position.x.is_finite() && packet.position.y.is_finite());
}
#[test]
fn a_touch_packet_yields_contact_geometry() {
let bytes = touch_bytes(
7,
POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT,
TOUCH_MASK_CONTACTAREA | TOUCH_MASK_PRESSURE,
(400, 300, 448, 336),
512,
);
let info = decode_touch_info(&bytes).expect("144 bytes is a whole POINTER_TOUCH_INFO");
assert_eq!(info.pointer_id, 7);
assert_eq!(info.contact, (400, 300, 448, 336));
assert_eq!(info.contact_size(2.0), Some(Size::new(24.0, 18.0)));
assert_eq!(info.contact_size(1.0), Some(Size::new(48.0, 36.0)));
assert_eq!(info.pressure_normalised(), Some(0.5));
}
#[test]
fn an_unreported_or_degenerate_contact_area_is_none() {
let unmasked = touch_bytes(7, 0, TOUCH_MASK_PRESSURE, (400, 300, 448, 336), 0);
assert_eq!(
decode_touch_info(&unmasked).unwrap().contact_size(1.0),
None
);
assert_eq!(
decode_touch_info(&unmasked).unwrap().pressure_normalised(),
Some(0.0)
);
let degenerate = touch_bytes(7, 0, TOUCH_MASK_CONTACTAREA, (400, 300, 400, 300), 0);
assert_eq!(
decode_touch_info(°enerate).unwrap().contact_size(1.0),
None,
"a zero-area rectangle is not a measurement"
);
}
#[test]
fn the_history_comes_back_oldest_first() {
let buffer = history_buffer(&[
history_entry(3_012, 900, (30, 30)),
history_entry(3_008, 600, (20, 20)),
history_entry(3_004, 300, (10, 10)),
]);
let decoded = decode_pen_history(&buffer, 3);
assert_eq!(decoded.len(), 3);
assert_eq!(
decoded.iter().map(|e| e.time_ms).collect::<Vec<_>>(),
vec![3_004, 3_008, 3_012],
"time must run forwards through the decoded run"
);
assert_eq!(
decoded.iter().map(|e| e.pressure).collect::<Vec<_>>(),
vec![300, 600, 900],
"and so must the pressure ramp: a reversed stroke starts hard and \
ends soft, which is exactly what a bad sort looks like"
);
assert_eq!(
decoded.iter().map(|e| e.screen).collect::<Vec<_>>(),
vec![(10, 10), (20, 20), (30, 30)]
);
}
#[test]
fn every_history_entry_keeps_its_own_axes_and_stamp() {
let buffer = history_buffer(&[
history_entry(80, 1_024, (5, 5)),
history_entry(76, 512, (4, 4)),
]);
let decoded = decode_pen_history(&buffer, 2);
let packets: Vec<_> = decoded.iter().map(|e| e.to_packet((0, 0), 1.0)).collect();
assert_eq!(packets[0].device_time_ms, Some(76));
assert_eq!(packets[1].device_time_ms, Some(80));
assert_eq!(packets[0].pressure, 0.5);
assert_eq!(packets[1].pressure, 1.0);
assert!(packets.iter().all(|p| p.down && p.in_proximity));
}
#[test]
fn a_short_history_buffer_yields_only_whole_entries() {
let buffer = history_buffer(&[
history_entry(20, 100, (1, 1)),
history_entry(10, 50, (0, 0)),
]);
let decoded = decode_pen_history(&buffer, 4);
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].time_ms, 10, "still oldest first");
let truncated = &buffer[..buffer.len() - 1];
let decoded = decode_pen_history(truncated, 2);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].time_ms, 20, "the only whole entry is the newest");
assert!(decode_pen_history(&[], 3).is_empty());
assert!(decode_pen_history(&buffer, 0).is_empty());
}
#[test]
fn the_history_request_is_bounded_and_skipped_when_empty() {
assert_eq!(history_entries_to_request(0), None, "no field, no call");
assert_eq!(history_entries_to_request(1), None, "nothing coalesced");
assert_eq!(history_entries_to_request(2), Some(2));
assert_eq!(
history_entries_to_request(MAX_PEN_HISTORY_ENTRIES as u32),
Some(MAX_PEN_HISTORY_ENTRIES)
);
assert_eq!(
history_entries_to_request(u32::MAX),
Some(MAX_PEN_HISTORY_ENTRIES),
"a nonsense count must not become a nonsense allocation"
);
}
#[test]
fn a_coalesced_message_back_dates_to_the_digitizers_spacing() {
use crate::pen::back_date;
use teksilo_core::pointer::EventTime;
let buffer = history_buffer(&[
history_entry(5_012, 900, (30, 30)),
history_entry(5_008, 600, (20, 20)),
history_entry(5_004, 300, (10, 10)),
]);
let packets: Vec<_> = decode_pen_history(&buffer, 3)
.iter()
.map(|e| e.to_packet((0, 0), 1.0))
.collect();
let stamps: Vec<_> = packets.iter().map(|p| p.device_time_ms).collect();
let now = EventTime::from_millis(90_000);
let times = back_date(now, &stamps);
assert_eq!(
times,
vec![
EventTime::from_millis(89_992),
EventTime::from_millis(89_996),
now,
],
"4 ms apart, which is what the digitizer said and not what the poll did"
);
}