1use chromiumoxide_cdp::cdp::browser_protocol::input::{
2 DispatchTouchEventParams, DispatchTouchEventType, TouchPoint,
3};
4use tokio::sync::oneshot;
5
6use crate::error::NightFuryError;
7use crate::session::BrowserSession;
8use crate::worker::WorkerState;
9
10#[non_exhaustive]
16pub enum TouchCmd {
17 Tap {
18 x: f64,
19 y: f64,
20 reply: oneshot::Sender<Result<String, String>>,
21 },
22 Swipe {
23 start_x: f64,
24 start_y: f64,
25 end_x: f64,
26 end_y: f64,
27 reply: oneshot::Sender<Result<String, String>>,
28 },
29}
30
31impl TouchCmd {
36 pub(crate) async fn dispatch(self, state: &mut WorkerState) {
37 match self {
38 TouchCmd::Tap { x, y, reply } => handle_tap(state, x, y, reply).await,
39 TouchCmd::Swipe {
40 start_x,
41 start_y,
42 end_x,
43 end_y,
44 reply,
45 } => handle_swipe(state, start_x, start_y, end_x, end_y, reply).await,
46 }
47 }
48}
49
50const SWIPE_STEPS: usize = 10;
56
57const SWIPE_STEP_DELAY_MS: u64 = 16;
59
60async fn handle_tap(
61 state: &mut WorkerState,
62 x: f64,
63 y: f64,
64 reply: oneshot::Sender<Result<String, String>>,
65) {
66 let result: Result<String, String> = async {
67 let page = &state.tabs[state.active_tab].page;
68 let raw = page.raw_page();
69 let point = TouchPoint::new(x, y);
70
71 raw.execute(DispatchTouchEventParams::new(
73 DispatchTouchEventType::TouchStart,
74 vec![point.clone()],
75 ))
76 .await
77 .map_err(|e| format!("Input.dispatchTouchEvent (touchStart) failed: {e}"))?;
78
79 raw.execute(DispatchTouchEventParams::new(
81 DispatchTouchEventType::TouchEnd,
82 vec![],
83 ))
84 .await
85 .map_err(|e| format!("Input.dispatchTouchEvent (touchEnd) failed: {e}"))?;
86
87 Ok(format!("Tapped at ({x:.0}, {y:.0})"))
88 }
89 .await;
90 let _ = reply.send(result);
91}
92
93async fn handle_swipe(
94 state: &mut WorkerState,
95 start_x: f64,
96 start_y: f64,
97 end_x: f64,
98 end_y: f64,
99 reply: oneshot::Sender<Result<String, String>>,
100) {
101 let result: Result<String, String> = async {
102 let page = &state.tabs[state.active_tab].page;
103 let raw = page.raw_page();
104
105 let start_point = TouchPoint::new(start_x, start_y);
107 raw.execute(DispatchTouchEventParams::new(
108 DispatchTouchEventType::TouchStart,
109 vec![start_point],
110 ))
111 .await
112 .map_err(|e| format!("Input.dispatchTouchEvent (touchStart) failed: {e}"))?;
113
114 for i in 1..=SWIPE_STEPS {
116 let t = i as f64 / SWIPE_STEPS as f64;
117 let cx = start_x + (end_x - start_x) * t;
118 let cy = start_y + (end_y - start_y) * t;
119 let move_point = TouchPoint::new(cx, cy);
120 raw.execute(DispatchTouchEventParams::new(
121 DispatchTouchEventType::TouchMove,
122 vec![move_point],
123 ))
124 .await
125 .map_err(|e| format!("Input.dispatchTouchEvent (touchMove) failed: {e}"))?;
126
127 tokio::time::sleep(std::time::Duration::from_millis(SWIPE_STEP_DELAY_MS)).await;
128 }
129
130 raw.execute(DispatchTouchEventParams::new(
132 DispatchTouchEventType::TouchEnd,
133 vec![],
134 ))
135 .await
136 .map_err(|e| format!("Input.dispatchTouchEvent (touchEnd) failed: {e}"))?;
137
138 Ok(format!(
139 "Swiped from ({start_x:.0}, {start_y:.0}) to ({end_x:.0}, {end_y:.0})"
140 ))
141 }
142 .await;
143 let _ = reply.send(result);
144}
145
146impl BrowserSession {
151 pub async fn tap(&self, x: f64, y: f64) -> Result<String, NightFuryError> {
157 send_cmd!(
158 self,
159 |tx| crate::cmd::BrowserCmd::Touch(TouchCmd::Tap { x, y, reply: tx }),
160 NightFuryError::OperationFailed
161 )
162 }
163
164 pub async fn swipe(
177 &self,
178 start_x: f64,
179 start_y: f64,
180 end_x: f64,
181 end_y: f64,
182 ) -> Result<String, NightFuryError> {
183 send_cmd!(
184 self,
185 |tx| crate::cmd::BrowserCmd::Touch(TouchCmd::Swipe {
186 start_x,
187 start_y,
188 end_x,
189 end_y,
190 reply: tx,
191 }),
192 NightFuryError::OperationFailed
193 )
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn touch_cmd_is_non_exhaustive() {
203 let (_tx, _rx) = oneshot::channel();
205 let _cmd = TouchCmd::Tap {
206 x: 100.0,
207 y: 200.0,
208 reply: _tx,
209 };
210 }
211
212 #[test]
213 fn swipe_cmd_fields() {
214 let (tx, _rx) = oneshot::channel();
215 let cmd = TouchCmd::Swipe {
216 start_x: 10.0,
217 start_y: 20.0,
218 end_x: 30.0,
219 end_y: 40.0,
220 reply: tx,
221 };
222 match cmd {
223 TouchCmd::Swipe {
224 start_x,
225 start_y,
226 end_x,
227 end_y,
228 ..
229 } => {
230 assert!((start_x - 10.0).abs() < f64::EPSILON);
231 assert!((start_y - 20.0).abs() < f64::EPSILON);
232 assert!((end_x - 30.0).abs() < f64::EPSILON);
233 assert!((end_y - 40.0).abs() < f64::EPSILON);
234 }
235 _ => panic!("expected Swipe variant"),
236 }
237 }
238}