Skip to main content

night_fury_core/domains/
touch.rs

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// ---------------------------------------------------------------------------
11// Command enum
12// ---------------------------------------------------------------------------
13
14/// Commands for the touch domain (tap, swipe).
15#[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
31// ---------------------------------------------------------------------------
32// Dispatch
33// ---------------------------------------------------------------------------
34
35impl 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
50// ---------------------------------------------------------------------------
51// Handlers
52// ---------------------------------------------------------------------------
53
54/// Number of intermediate touch-move steps for a swipe gesture.
55const SWIPE_STEPS: usize = 10;
56
57/// Delay in milliseconds between swipe steps (simulates realistic finger speed).
58const 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        // touchStart
72        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        // touchEnd (must have empty touch_points per CDP spec)
80        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        // touchStart at the beginning
106        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        // Interpolated touchMove events
115        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        // touchEnd
131        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
146// ---------------------------------------------------------------------------
147// Session API
148// ---------------------------------------------------------------------------
149
150impl BrowserSession {
151    /// Simulate a touch tap at the given screen coordinates.
152    ///
153    /// Dispatches `touchStart` followed by `touchEnd` via CDP
154    /// `Input.dispatchTouchEvent`. The viewport should be configured with
155    /// `has_touch: true` and `emulating_mobile: true` for correct behaviour.
156    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    /// Simulate a touch swipe gesture between two points.
165    ///
166    /// Dispatches `touchStart`, a series of interpolated `touchMove` events,
167    /// and a final `touchEnd`. Use this for scroll-like gestures on
168    /// mobile-emulated pages.
169    ///
170    /// ```text
171    /// // Swipe up (scroll down):
172    /// session.swipe(200.0, 500.0, 200.0, 100.0).await?;
173    /// // Swipe left:
174    /// session.swipe(300.0, 400.0, 50.0, 400.0).await?;
175    /// ```
176    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        // Compile-time check: TouchCmd variants can be constructed.
204        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}