From 0345fccba2c89c6f7b015fa5c2e1203525a671f9 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 30 Oct 2026 22:46:01 -0700 Subject: [PATCH] refactor(komorebic): the macOS komorebic uses the pan module of komorebic-core komorebi-for-mac/komorebic/src/pan.rs (810 lines) is deleted: it differed from the Windows copy by three doc comments only (whkd wording versus a generic one, 21 changed lines), so the shared module keeps the Windows wording of those comments; no code differs. pan --stream runs komorebic_core::pan with a PanLink over the client connection. Verified: macOS fake-CC cargo check --locked --workspace --all-targets: 0 errors. The 16 pan tests of the macOS tree were the same tests as Windows'; they run in komorebic-core (27 passed). Not verified on macOS itself. --- komorebi-for-mac/komorebic/src/main.rs | 14 +- komorebi-for-mac/komorebic/src/pan.rs | 810 ------------------------- 2 files changed, 11 insertions(+), 813 deletions(-) delete mode 100644 komorebi-for-mac/komorebic/src/pan.rs diff --git a/komorebi-for-mac/komorebic/src/main.rs b/komorebi-for-mac/komorebic/src/main.rs index 3dff61e8..0e3b7b09 100644 --- a/komorebi-for-mac/komorebic/src/main.rs +++ b/komorebi-for-mac/komorebic/src/main.rs @@ -14,6 +14,7 @@ use komorebic_core::cli::Meta; use komorebic_core::cli::command; use komorebic_core::docgen::DocFormat; use komorebic_core::messages::Messages; +use komorebic_core::pan; use komorebic_core::platform::macos::*; use lazy_static::lazy_static; use std::fs::File; @@ -30,8 +31,6 @@ use std::sync::atomic::Ordering; use std::time::Duration; use sysinfo::ProcessesToUpdate; -mod pan; - lazy_static! { static ref HAS_CUSTOM_CONFIG_HOME: AtomicBool = AtomicBool::new(false); static ref HOME_DIR: PathBuf = { @@ -82,6 +81,15 @@ fn send_or_query(messages: Messages) -> eyre::Result<()> { Ok(()) } +/// The connection to the daemon that the gesture stream tells what the fingers do over. +struct PanLink(komorebi_client::Connection); + +impl pan::Link for PanLink { + fn send(&mut self, message: &SocketMessage) -> io::Result<()> { + self.0.send(message) + } +} + fn main() -> eyre::Result<()> { let opts = komorebic_core::cli::parse::(&META); @@ -775,7 +783,7 @@ exit 1 reversed: args.reversed, paging: args.paging, }, - &mut pan::Socket::new(komorebi_client::Connection::open), + &mut pan::Socket::new(|| komorebi_client::Connection::open().map(PanLink)), )?; } else if let Some(direction) = args.direction { send_message(&SocketMessage::Pan(direction))?; diff --git a/komorebi-for-mac/komorebic/src/pan.rs b/komorebi-for-mac/komorebic/src/pan.rs deleted file mode 100644 index dc3c0d9a..00000000 --- a/komorebi-for-mac/komorebic/src/pan.rs +++ /dev/null @@ -1,810 +0,0 @@ -//! Reading the stream of gestures that `komorebic pan --stream` is given, which is what a program -//! that reads a touchpad writes for a swipe of fingers, and telling komorebi what the fingers do. -//! -//! The format is the gesture stream that whkd writes on Windows, and anything that writes it can -//! be piped in: one message to a line, a name and then `key=value` fields in any order, the -//! distances in centimetres. A field that is not known is ignored, and so is a message that is not -//! known. - -use komorebi_client::PanDirection; -use komorebi_client::PanGesture; -use komorebi_client::SocketMessage; -use std::collections::VecDeque; -use std::io; -use std::io::BufRead; -use std::sync::mpsc; - -/// How far the fingers go with `--paging` for each step, in centimetres -pub const PAGE_CM: f64 = 2.0; - -/// One thing that the fingers do -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Message { - Begin, - Move { dx: f64, dy: f64, t: f64 }, - End, - Cancel, -} - -/// What a line of the stream comes to -#[derive(Clone, Debug, PartialEq)] -pub enum Line { - Message(Message), - /// Nothing to do: a line with nothing in it, or a comment, or a message that is not known - Ignored, - /// A message that is known, and is not what it should be - Bad(String), -} - -/// What a line of the stream says -#[must_use] -pub fn parse_line(line: &str) -> Line { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - return Line::Ignored; - } - - let mut words = line.split_whitespace(); - let Some(name) = words.next() else { - return Line::Ignored; - }; - - let fields: Vec<(&str, &str)> = words.filter_map(|word| word.split_once('=')).collect(); - let number = |key: &str| -> Option> { - fields - .iter() - .find(|(name, _)| *name == key) - .map(|(_, value)| { - value - .parse::() - .ok() - .filter(|number| number.is_finite()) - .ok_or_else(|| format!("{key}={value} is not a number")) - }) - }; - - match name { - "begin" => Line::Message(Message::Begin), - "end" => Line::Message(Message::End), - "cancel" => Line::Message(Message::Cancel), - "move" => { - let dx = match number("dx") { - Some(Ok(dx)) => dx, - Some(Err(error)) => return Line::Bad(error), - None => return Line::Bad(String::from("a move has no dx")), - }; - let t = match number("t") { - Some(Ok(t)) => t, - Some(Err(error)) => return Line::Bad(error), - None => return Line::Bad(String::from("a move has no t")), - }; - let dy = match number("dy") { - Some(Ok(dy)) => dy, - Some(Err(error)) => return Line::Bad(error), - None => 0.0, - }; - - Line::Message(Message::Move { dx, dy, t }) - } - _ => Line::Ignored, - } -} - -/// What `--stream` is asked to do with the distances -#[derive(Clone, Copy, Debug)] -pub struct Options { - /// What the distances are multiplied by - pub sensitivity: f64, - /// The strip goes the other way to the fingers - pub reversed: bool, - /// One step for each [`PAGE_CM`] that the fingers go, and not the strip going with them - pub paging: bool, -} - -impl Default for Options { - fn default() -> Self { - Self { - sensitivity: 1.0, - reversed: false, - paging: false, - } - } -} - -/// Works out what komorebi is told for what the fingers do -#[derive(Debug)] -pub struct Forwarder { - options: Options, - /// Whether a gesture is going on - active: bool, - /// How far the fingers have gone since the last step, with paging - page: f64, -} - -impl Forwarder { - #[must_use] - pub const fn new(options: Options) -> Self { - Self { - options, - active: false, - page: 0.0, - } - } - - /// What komorebi is told for this. Fingers that move when there was no beginning, and an end - /// or a cancel for a gesture that is not going on, are not anything. - pub fn handle(&mut self, message: Message) -> Vec { - let gesture = |gesture| vec![SocketMessage::PanGesture(gesture)]; - - match message { - Message::Begin => { - self.active = true; - self.page = 0.0; - - if self.options.paging { - Vec::new() - } else { - gesture(PanGesture::Begin) - } - } - Message::Move { dx, dy, t } if self.active => { - let sign = if self.options.reversed { -1.0 } else { 1.0 }; - let dx = dx * self.options.sensitivity * sign; - let dy = dy * self.options.sensitivity; - - if self.options.paging { - self.page += dx; - self.pages() - } else { - gesture(PanGesture::Move { dx, dy, t }) - } - } - Message::End if self.active => { - self.active = false; - - if self.options.paging { - Vec::new() - } else { - gesture(PanGesture::End) - } - } - Message::Cancel if self.active => { - self.active = false; - - if self.options.paging { - Vec::new() - } else { - gesture(PanGesture::Cancel) - } - } - _ => Vec::new(), - } - } - - /// The stream is over: a gesture that is still going on was not finished, so it is given up - pub fn finish(&mut self) -> Vec { - let unfinished = self.active && !self.options.paging; - self.active = false; - - if unfinished { - vec![SocketMessage::PanGesture(PanGesture::Cancel)] - } else { - Vec::new() - } - } - - /// The steps that what the fingers have gone since the last one comes to. Fingers that go to - /// the right take the strip with them, which is to look to the left. - fn pages(&mut self) -> Vec { - let mut steps = Vec::new(); - - while self.page.abs() >= PAGE_CM { - let direction = if self.page > 0.0 { - PanDirection::Left - } else { - PanDirection::Right - }; - - self.page -= PAGE_CM.copysign(self.page); - steps.push(SocketMessage::Pan(direction)); - } - - steps - } -} - -/// The next message to handle, with the moves that follow it added to it if it is one: when -/// the stream comes faster than it can be handled, the distances add up and the time is the -/// last one, as whkd says that they may. -pub fn next_merged(queue: &mut VecDeque) -> Option { - let first = queue.pop_front()?; - let Message::Move { - mut dx, - mut dy, - mut t, - } = first - else { - return Some(first); - }; - - while let Some(Message::Move { - dx: more_dx, - dy: more_dy, - t: later, - }) = queue.front().copied() - { - dx += more_dx; - dy += more_dy; - t = later; - queue.pop_front(); - } - - Some(Message::Move { dx, dy, t }) -} - -/// Where komorebi is told what the fingers do -pub trait Sink { - fn send(&mut self, message: &SocketMessage) -> io::Result<()>; - - /// The gesture that was being told is over - fn gesture_over(&mut self); -} - -/// A way of telling komorebi things, in order -pub trait Link { - fn send(&mut self, message: &SocketMessage) -> io::Result<()>; -} - -impl Link for komorebi_client::Connection { - fn send(&mut self, message: &SocketMessage) -> io::Result<()> { - komorebi_client::Connection::send(self, message) - } -} - -/// Tells komorebi what the fingers do over one connection for each gesture. Messages that are -/// sent on a connection each are not handled in the order that they were sent, so a gesture is -/// told on one, which is made when it is first needed and let go of when the gesture is over. -/// If it cannot be written to the connection is made again once, as komorebi may have let go of -/// one that nothing has been written to for a while. -pub struct Socket { - open: F, - link: Option, -} - -impl io::Result> Socket { - pub const fn new(open: F) -> Self { - Self { open, link: None } - } - - fn try_send(&mut self, message: &SocketMessage) -> io::Result<()> { - if self.link.is_none() { - self.link = Some((self.open)()?); - } - - match self.link.as_mut() { - Some(link) => link.send(message), - None => Ok(()), - } - } -} - -impl io::Result> Sink for Socket { - fn send(&mut self, message: &SocketMessage) -> io::Result<()> { - if self.try_send(message).is_err() { - self.link = None; - return self.try_send(message); - } - - Ok(()) - } - - fn gesture_over(&mut self) { - self.link = None; - } -} - -/// Read gestures from `input` until it closes and tell komorebi what the fingers do with `sink` -pub fn run( - input: impl BufRead + Send + 'static, - options: Options, - sink: &mut impl Sink, -) -> io::Result<()> { - let (sender, receiver) = mpsc::channel(); - - std::thread::spawn(move || { - for line in input.lines() { - let Ok(line) = line else { - break; - }; - - match parse_line(&line) { - Line::Message(message) => { - if sender.send(message).is_err() { - break; - } - } - Line::Ignored => {} - Line::Bad(why) => eprintln!("pan: {why}: {line}"), - } - } - }); - - let mut forwarder = Forwarder::new(options); - let mut queue = VecDeque::new(); - - while let Ok(first) = receiver.recv() { - queue.push_back(first); - queue.extend(receiver.try_iter()); - - while let Some(message) = next_merged(&mut queue) { - let over = matches!(message, Message::End | Message::Cancel); - - for told in forwarder.handle(message) { - sink.send(&told)?; - } - - if over { - sink.gesture_over(); - } - } - } - - for told in forwarder.finish() { - sink.send(&told)?; - } - sink.gesture_over(); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - const TRANSCRIPT: &str = include_str!("../fixtures/gesture-stream.transcript.txt"); - - fn gestures(told: &[SocketMessage]) -> Vec { - told.iter() - .filter_map(|message| match message { - SocketMessage::PanGesture(gesture) => Some(*gesture), - _ => None, - }) - .collect() - } - - fn messages(text: &str) -> Vec { - text.lines() - .filter_map(|line| match parse_line(line) { - Line::Message(message) => Some(message), - _ => None, - }) - .collect() - } - - /// What is told, and how many gestures have been over - #[derive(Default)] - struct Recorder { - told: Vec, - over: usize, - } - - impl Sink for Recorder { - fn send(&mut self, message: &SocketMessage) -> io::Result<()> { - self.told.push(message.clone()); - Ok(()) - } - - fn gesture_over(&mut self) { - self.over += 1; - } - } - - /// A link that remembers what it was written, and whose connections are numbered - struct Wire { - connection: usize, - written: Written, - fail_next: Failing, - } - - impl Link for Wire { - fn send(&mut self, message: &SocketMessage) -> io::Result<()> { - if self.fail_next.replace(false) { - return Err(io::Error::new(io::ErrorKind::BrokenPipe, "let go of")); - } - - self.written - .borrow_mut() - .push((self.connection, message.clone())); - Ok(()) - } - } - - type Written = std::rc::Rc>>; - - type Failing = std::rc::Rc>; - - #[allow(clippy::type_complexity)] - fn wire() -> ( - Socket io::Result>, - Written, - Failing, - ) { - let written = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); - let fail_next = std::rc::Rc::new(std::cell::Cell::new(false)); - let mut made = 0; - - let socket = Socket::new({ - let written = written.clone(); - let fail_next = fail_next.clone(); - move || { - made += 1; - Ok(Wire { - connection: made, - written: written.clone(), - fail_next: fail_next.clone(), - }) - } - }); - - (socket, written, fail_next) - } - - #[test] - fn a_gesture_is_told_on_one_connection_and_the_next_has_its_own() { - let (mut socket, written, _) = wire(); - let message = |gesture| SocketMessage::PanGesture(gesture); - let moved = PanGesture::Move { - dx: 1.0, - dy: 0.0, - t: 1.0, - }; - - socket.send(&message(PanGesture::Begin)).unwrap(); - socket.send(&message(moved)).unwrap(); - socket.send(&message(moved)).unwrap(); - socket.send(&message(PanGesture::End)).unwrap(); - socket.gesture_over(); - socket.send(&message(PanGesture::Begin)).unwrap(); - - let connections: Vec<_> = written.borrow().iter().map(|(number, _)| *number).collect(); - assert_eq!(connections, vec![1, 1, 1, 1, 2]); - } - - #[test] - fn a_connection_that_komorebi_let_go_of_is_made_again_once() { - let (mut socket, written, fail_next) = wire(); - let message = SocketMessage::PanGesture(PanGesture::Begin); - - socket.send(&message).unwrap(); - fail_next.set(true); - socket.send(&message).unwrap(); - - let connections: Vec<_> = written.borrow().iter().map(|(number, _)| *number).collect(); - assert_eq!( - connections, - vec![1, 2], - "the second was written on a new one" - ); - } - - #[test] - fn a_message_that_cannot_be_written_twice_is_an_error() { - let mut socket = Socket::new(|| -> io::Result { - Err(io::Error::new(io::ErrorKind::NotFound, "no komorebi")) - }); - - assert!( - socket - .send(&SocketMessage::PanGesture(PanGesture::Begin)) - .is_err() - ); - } - - #[test] - fn what_is_told_ends_each_gesture_that_ends_and_the_stream_that_stops() { - let input = b"begin\nmove dx=1 t=1\nend\nbegin\nmove dx=1 t=2\n".to_vec(); - let mut sink = Recorder::default(); - - run(io::Cursor::new(input), Options::default(), &mut sink).unwrap(); - - // the first ended, and the second was given up when the stream stopped - assert_eq!(sink.over, 2); - assert_eq!(gestures(&sink.told).last(), Some(&PanGesture::Cancel)); - } - - fn step(dx: f64) -> Message { - Message::Move { - dx, - dy: 0.0, - t: 0.0, - } - } - - #[test] - fn what_whkd_writes_is_read_as_it_was_written() { - assert_eq!( - messages(TRANSCRIPT), - vec![ - Message::Begin, - Message::Move { - dx: 0.42, - dy: -0.03, - t: 118_345.0 - }, - Message::Move { - dx: 0.51, - dy: -0.02, - t: 118_353.0 - }, - Message::Move { - dx: 0.47, - dy: 0.0, - t: 118_361.0 - }, - Message::End, - ] - ); - } - - /// What whkd writes for a swipe of three fingers that goes on for about two seconds, a - /// flick, and one that eases to a stop: `whkd touchpad --demo --instant` - const LONG: &str = include_str!("../fixtures/pan3-long.transcript.txt"); - const FLICK: &str = include_str!("../fixtures/pan3-flick.transcript.txt"); - const SLOW: &str = include_str!("../fixtures/pan3-slow.transcript.txt"); - - /// How many lines of a transcript are not part of the stream: whkd's demo says which swipe - /// it saw, as `swipe swipe3right`, among them, and nothing says that the program that it - /// runs is told it, so it is ignored like any other message that is not known - fn others(transcript: &str) -> usize { - transcript - .lines() - .filter(|line| line.starts_with("swipe ")) - .count() - } - - #[test] - fn every_line_that_whkd_writes_for_a_swipe_is_read_and_nothing_is_said_to_be_wrong() { - for (transcript, lines) in [(LONG, 218), (FLICK, 18), (SLOW, 67)] { - assert_eq!(transcript.lines().count(), lines); - - for line in transcript.lines() { - match parse_line(line) { - Line::Message(_) => {} - Line::Ignored => assert!(line.starts_with("swipe "), "ignored: {line}"), - Line::Bad(why) => panic!("{why}: {line}"), - } - } - - let messages = messages(transcript); - assert_eq!(messages.first(), Some(&Message::Begin)); - assert_eq!(messages.last(), Some(&Message::End)); - assert_eq!(messages.len(), lines - others(transcript)); - } - - assert_eq!(others(LONG) + others(FLICK) + others(SLOW), 2); - } - - #[test] - fn a_swipe_is_told_to_komorebi_move_for_move() { - for transcript in [LONG, FLICK, SLOW] { - let mut forwarder = Forwarder::new(Options::default()); - let told: Vec<_> = messages(transcript) - .into_iter() - .flat_map(|message| forwarder.handle(message)) - .collect(); - - let gestures = gestures(&told); - assert_eq!( - gestures.len(), - transcript.lines().count() - others(transcript) - ); - assert_eq!(gestures.first(), Some(&PanGesture::Begin)); - assert_eq!(gestures.last(), Some(&PanGesture::End)); - - // the distances add up to how far the fingers went across the touchpad - let across: f64 = gestures - .iter() - .filter_map(|gesture| match gesture { - PanGesture::Move { dx, .. } => Some(*dx), - _ => None, - }) - .sum(); - assert!(across > 1.0, "{across}"); - } - } - - #[test] - fn a_field_that_is_not_known_is_not_anything_and_nor_is_a_message() { - // x, y, w, h, and a field of the future, which whkd says to ignore without a word - assert_eq!( - parse_line("move dx=1.5 dy=0.25 x=4.420 y=1.970 t=10 pressure=0.7"), - Line::Message(Message::Move { - dx: 1.5, - dy: 0.25, - t: 10.0 - }) - ); - assert_eq!( - parse_line("begin fingers=3 x=4.000 y=2.000 w=10.500 h=7.200 t=118337"), - Line::Message(Message::Begin) - ); - assert_eq!( - parse_line("end x=5.4 y=1.95 t=118369"), - Line::Message(Message::End) - ); - assert_eq!(parse_line("pinch scale=1.1"), Line::Ignored); - assert_eq!(parse_line(""), Line::Ignored); - assert_eq!(parse_line(" "), Line::Ignored); - assert_eq!(parse_line("# a comment"), Line::Ignored); - } - - #[test] - fn a_move_that_is_not_what_it_should_be_is_said_to_be() { - assert!(matches!(parse_line("move dy=1 t=3"), Line::Bad(_)), "no dx"); - assert!(matches!(parse_line("move dx=1 dy=1"), Line::Bad(_)), "no t"); - assert!(matches!(parse_line("move dx=wide t=3"), Line::Bad(_))); - assert!(matches!(parse_line("move dx=NaN t=3"), Line::Bad(_))); - assert_eq!( - parse_line("move dx=2 t=3"), - Line::Message(Message::Move { - dx: 2.0, - dy: 0.0, - t: 3.0 - }), - "dy is not needed" - ); - } - - #[test] - fn the_strip_goes_with_the_fingers_and_the_times_are_told_as_they_are() { - let mut forwarder = Forwarder::new(Options::default()); - let told: Vec<_> = messages(TRANSCRIPT) - .into_iter() - .flat_map(|message| forwarder.handle(message)) - .collect(); - - let gestures = gestures(&told); - assert_eq!(gestures.len(), 5); - assert_eq!(gestures[0], PanGesture::Begin); - assert_eq!( - gestures[1], - PanGesture::Move { - dx: 0.42, - dy: -0.03, - t: 118_345.0 - } - ); - assert_eq!(gestures[4], PanGesture::End); - assert!( - forwarder.finish().is_empty(), - "it ended, so nothing is left" - ); - } - - #[test] - fn sensitivity_scales_the_distances_and_reversing_flips_the_way_it_goes() { - let mut forwarder = Forwarder::new(Options { - sensitivity: 2.0, - reversed: true, - paging: false, - }); - forwarder.handle(Message::Begin); - - let told = forwarder.handle(Message::Move { - dx: 1.0, - dy: 0.5, - t: 1.0, - }); - - assert_eq!( - gestures(&told), - vec![PanGesture::Move { - dx: -2.0, - dy: 1.0, - t: 1.0 - }] - ); - } - - #[test] - fn what_has_no_beginning_is_not_anything() { - let mut forwarder = Forwarder::new(Options::default()); - - assert!(forwarder.handle(step(1.0)).is_empty()); - assert!(forwarder.handle(Message::End).is_empty()); - assert!(forwarder.handle(Message::Cancel).is_empty()); - assert!(forwarder.finish().is_empty()); - } - - #[test] - fn a_stream_that_stops_in_the_middle_of_a_gesture_gives_it_up() { - let mut forwarder = Forwarder::new(Options::default()); - forwarder.handle(Message::Begin); - forwarder.handle(step(1.0)); - - assert_eq!(gestures(&forwarder.finish()), vec![PanGesture::Cancel]); - assert!(forwarder.finish().is_empty(), "once"); - } - - #[test] - fn fingers_that_go_far_enough_take_a_step_each_with_paging() { - let mut forwarder = Forwarder::new(Options { - paging: true, - ..Options::default() - }); - - assert!(forwarder.handle(Message::Begin).is_empty()); - assert!(forwarder.handle(step(1.5)).is_empty(), "not far enough"); - - // 1.5 and 0.7 is 2.2: a step to the left, as the strip goes right with the fingers - let told = forwarder.handle(step(0.7)); - assert!(matches!( - told.as_slice(), - [SocketMessage::Pan(PanDirection::Left)] - )); - - // 0.2 is left over, and with 4.0 more it is two steps - let told = forwarder.handle(step(4.0)); - assert_eq!(told.len(), 2); - - // and the other way: 0.2 and 8.5 back is 8.3, which is four - let told = forwarder.handle(step(-8.5)); - assert_eq!(told.len(), 4); - assert!( - told.iter() - .all(|message| matches!(message, SocketMessage::Pan(PanDirection::Right))) - ); - - assert!(forwarder.handle(Message::End).is_empty()); - assert!(forwarder.finish().is_empty()); - } - - #[test] - fn moves_that_wait_are_added_up_and_nothing_else_is_put_out_of_its_place() { - let mut queue: VecDeque = [ - Message::Begin, - Message::Move { - dx: 1.0, - dy: 0.5, - t: 1.0, - }, - Message::Move { - dx: 2.0, - dy: 0.25, - t: 2.0, - }, - Message::Move { - dx: 4.0, - dy: 0.0, - t: 3.0, - }, - Message::End, - ] - .into(); - - assert_eq!(next_merged(&mut queue), Some(Message::Begin)); - assert_eq!( - next_merged(&mut queue), - Some(Message::Move { - dx: 7.0, - dy: 0.75, - t: 3.0 - }) - ); - assert_eq!(next_merged(&mut queue), Some(Message::End)); - assert_eq!(next_merged(&mut queue), None); - } - - #[test] - fn a_stream_is_told_to_komorebi_in_order_and_given_up_if_it_stops() { - let input = b"begin fingers=3\nmove dx=1.0 dy=0 t=1\nmove dx=2.0 dy=0 t=2\n".to_vec(); - let mut sink = Recorder::default(); - - run(io::Cursor::new(input), Options::default(), &mut sink).unwrap(); - - let gestures = gestures(&sink.told); - assert_eq!(gestures[0], PanGesture::Begin); - assert!(matches!(gestures[1], PanGesture::Move { .. })); - assert_eq!(gestures.last(), Some(&PanGesture::Cancel)); - } -}