cdp_core/input/
mouse.rs

1use std::{
2    future::Future,
3    sync::Arc,
4    time::{Duration, Instant},
5};
6
7use rand::Rng;
8use serde_json::Value;
9
10pub use cdp_protocol::input::MouseButton;
11use cdp_protocol::input::{
12    DispatchMouseEvent, DispatchMouseEventReturnObject, DispatchMouseEventTypeOption,
13};
14use cdp_protocol::runtime::{Evaluate, EvaluateReturnObject};
15
16use crate::{
17    domain_manager::DomainManager,
18    error::{CdpError, Result},
19    session::Session,
20};
21
22/// Configuration for a single mouse click.
23#[derive(Debug, Clone)]
24pub struct MouseClickOptions {
25    /// Mouse button to use; defaults to the left button.
26    pub button: MouseButton,
27    /// Optional delay to wait before releasing the button.
28    pub delay_after_press: Option<Duration>,
29}
30
31impl Default for MouseClickOptions {
32    fn default() -> Self {
33        Self {
34            button: MouseButton::Left,
35            delay_after_press: None,
36        }
37    }
38}
39
40/// Configuration for a double-click action.
41#[derive(Debug, Clone)]
42pub struct DoubleClickOptions {
43    /// Mouse button to use; defaults to the left button.
44    pub button: MouseButton,
45    /// Delay inserted between the two presses.
46    pub delay_between_clicks: Duration,
47}
48
49impl Default for DoubleClickOptions {
50    fn default() -> Self {
51        Self {
52            button: MouseButton::Left,
53            delay_between_clicks: Duration::from_millis(50),
54        }
55    }
56}
57
58/// Options for holding the mouse button until a condition is met.
59#[derive(Debug, Clone)]
60pub struct PressHoldOptions {
61    /// Mouse button to use; defaults to the left button.
62    pub button: MouseButton,
63    /// Interval between polling the condition callback.
64    pub poll_interval: Duration,
65    /// Maximum amount of time to wait; `None` disables the timeout.
66    pub timeout: Option<Duration>,
67    /// Minimum duration to hold the button before evaluating the condition.
68    pub min_duration: Option<Duration>,
69}
70
71impl Default for PressHoldOptions {
72    fn default() -> Self {
73        Self {
74            button: MouseButton::Left,
75            poll_interval: Duration::from_millis(100),
76            timeout: None,
77            min_duration: None,
78        }
79    }
80}
81
82/// Speed curve used while generating drag motion.
83#[derive(Debug, Clone, Copy, Default)]
84pub enum DragEasing {
85    /// Linear speed, without acceleration.
86    Linear,
87    /// Smooth ease-in/ease-out cubic profile.
88    #[default]
89    EaseInOutCubic,
90    /// Quick start with a slower finish.
91    EaseOutQuad,
92}
93
94impl DragEasing {
95    fn evaluate(self, t: f64) -> f64 {
96        let clamped = t.clamp(0.0, 1.0);
97        match self {
98            DragEasing::Linear => clamped,
99            DragEasing::EaseInOutCubic => {
100                if clamped < 0.5 {
101                    4.0 * clamped * clamped * clamped
102                } else {
103                    let s = -2.0 * clamped + 2.0;
104                    1.0 - (s * s * s) / 2.0
105                }
106            }
107            DragEasing::EaseOutQuad => 1.0 - (1.0 - clamped) * (1.0 - clamped),
108        }
109    }
110}
111
112/// Configuration controlling drag gestures.
113#[derive(Debug, Clone)]
114pub struct DragOptions {
115    /// Mouse button to use; defaults to the left button.
116    pub button: MouseButton,
117    /// Total duration of the drag gesture.
118    pub total_duration: Duration,
119    /// Number of intermediate steps; higher values produce smoother motion.
120    pub steps: usize,
121    /// Maximum jitter, in pixels, applied to intermediate points.
122    pub jitter_px: f64,
123    /// Optional delay after pressing the button and before moving.
124    pub hold_before_move: Option<Duration>,
125    /// Optional delay after reaching the target and before releasing.
126    pub settle_after_move: Option<Duration>,
127    /// Whether the cursor should move to the start before pressing.
128    pub move_cursor_to_start: bool,
129    /// Curve used to distribute motion during the drag.
130    pub easing: DragEasing,
131}
132
133impl Default for DragOptions {
134    fn default() -> Self {
135        Self {
136            button: MouseButton::Left,
137            total_duration: Duration::from_millis(700),
138            steps: 28,
139            jitter_px: 0.8,
140            hold_before_move: Some(Duration::from_millis(80)),
141            settle_after_move: Some(Duration::from_millis(60)),
142            move_cursor_to_start: true,
143            easing: DragEasing::default(),
144        }
145    }
146}
147
148/// High-level helper for dispatching mouse input via the CDP Input domain.
149#[derive(Clone)]
150pub struct Mouse {
151    session: Arc<Session>,
152    domain_manager: Arc<DomainManager>,
153}
154
155/// Current mouse coordinates in both viewport and screen space.
156#[derive(Debug, Clone, Copy, PartialEq)]
157pub struct MousePosition {
158    /// X coordinate relative to the top-left of the viewport (CSS pixels).
159    pub viewport_x: f64,
160    /// Y coordinate relative to the top-left of the viewport (CSS pixels).
161    pub viewport_y: f64,
162    /// X coordinate relative to the top-left of the screen (CSS pixels).
163    pub screen_x: f64,
164    /// Y coordinate relative to the top-left of the screen (CSS pixels).
165    pub screen_y: f64,
166}
167
168impl Mouse {
169    pub(crate) fn new(session: Arc<Session>, domain_manager: Arc<DomainManager>) -> Self {
170        Self {
171            session,
172            domain_manager,
173        }
174    }
175
176    async fn dispatch(&self, params: DispatchMouseEvent) -> Result<()> {
177        self.session
178            .send_command::<_, DispatchMouseEventReturnObject>(params, None)
179            .await
180            .map(|_| ())
181    }
182
183    /// Moves the cursor to the given coordinates and returns the resolved position.
184    ///
185    /// # Examples
186    /// ```no_run
187    /// # use cdp_core::Page;
188    /// # use std::sync::Arc;
189    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
190    /// let mouse = page.mouse();
191    /// let position = mouse.move_to(120.0, 80.0).await?;
192    /// assert_eq!(position.viewport_x, 120.0);
193    /// # Ok(())
194    /// # }
195    /// ```
196    pub async fn move_to(&self, x: f64, y: f64) -> Result<MousePosition> {
197        let params = DispatchMouseEvent {
198            r#type: DispatchMouseEventTypeOption::MouseMoved,
199            x,
200            y,
201            modifiers: None,
202            timestamp: None,
203            button: None,
204            buttons: Some(0),
205            click_count: Some(0),
206            force: None,
207            tangential_pressure: None,
208            tilt_x: None,
209            tilt_y: None,
210            twist: None,
211            delta_x: None,
212            delta_y: None,
213            pointer_type: None,
214        };
215
216        self.dispatch(params).await?;
217        self.position_for(x, y).await
218    }
219
220    async fn position_for(&self, viewport_x: f64, viewport_y: f64) -> Result<MousePosition> {
221        let expression = format!(
222            "(() => {{
223                const viewportX = {viewport_x};
224                const viewportY = {viewport_y};
225                const screenBaseX = window.screenX ?? window.screenLeft ?? 0;
226                const screenBaseY = window.screenY ?? window.screenTop ?? 0;
227                return {{
228                    viewportX,
229                    viewportY,
230                    screenX: screenBaseX + viewportX,
231                    screenY: screenBaseY + viewportY
232                }};
233            }})()",
234            viewport_x = viewport_x,
235            viewport_y = viewport_y,
236        );
237
238        let evaluate = Evaluate {
239            expression,
240            object_group: None,
241            include_command_line_api: None,
242            silent: None,
243            context_id: None,
244            return_by_value: Some(true),
245            generate_preview: None,
246            user_gesture: None,
247            await_promise: None,
248            throw_on_side_effect: None,
249            timeout: None,
250            disable_breaks: None,
251            repl_mode: None,
252            allow_unsafe_eval_blocked_by_csp: None,
253            unique_context_id: None,
254            serialization_options: None,
255        };
256
257        let eval_result = self
258            .session
259            .send_command::<_, EvaluateReturnObject>(evaluate, None)
260            .await?;
261
262        let value: Value = eval_result
263            .result
264            .value
265            .ok_or_else(|| CdpError::tool("Failed to obtain mouse position after move"))?;
266
267        let screen_x = value
268            .get("screenX")
269            .and_then(Value::as_f64)
270            .ok_or_else(|| CdpError::tool("Invalid screenX returned for mouse position"))?;
271        let screen_y = value
272            .get("screenY")
273            .and_then(Value::as_f64)
274            .ok_or_else(|| CdpError::tool("Invalid screenY returned for mouse position"))?;
275        let viewport_x = value
276            .get("viewportX")
277            .and_then(Value::as_f64)
278            .ok_or_else(|| CdpError::tool("Invalid viewportX returned for mouse position"))?;
279        let viewport_y = value
280            .get("viewportY")
281            .and_then(Value::as_f64)
282            .ok_or_else(|| CdpError::tool("Invalid viewportY returned for mouse position"))?;
283
284        Ok(MousePosition {
285            viewport_x,
286            viewport_y,
287            screen_x,
288            screen_y,
289        })
290    }
291
292    async fn press(&self, x: f64, y: f64, button: MouseButton, click_count: u32) -> Result<()> {
293        let params = DispatchMouseEvent {
294            r#type: DispatchMouseEventTypeOption::MousePressed,
295            x,
296            y,
297            modifiers: None,
298            timestamp: None,
299            button: Some(button.clone()),
300            buttons: None,
301            click_count: Some(click_count),
302            force: None,
303            tangential_pressure: None,
304            tilt_x: None,
305            tilt_y: None,
306            twist: None,
307            delta_x: None,
308            delta_y: None,
309            pointer_type: None,
310        };
311        self.dispatch(params).await
312    }
313
314    async fn release(&self, x: f64, y: f64, button: MouseButton, click_count: u32) -> Result<()> {
315        let params = DispatchMouseEvent {
316            r#type: DispatchMouseEventTypeOption::MouseReleased,
317            x,
318            y,
319            modifiers: None,
320            timestamp: None,
321            button: Some(button.clone()),
322            buttons: None,
323            click_count: Some(click_count),
324            force: None,
325            tangential_pressure: None,
326            tilt_x: None,
327            tilt_y: None,
328            twist: None,
329            delta_x: None,
330            delta_y: None,
331            pointer_type: None,
332        };
333        self.dispatch(params).await
334    }
335
336    /// Performs a single click at the provided coordinates.
337    ///
338    /// # Examples
339    /// ```no_run
340    /// # use cdp_core::{MouseClickOptions, MouseButton, Page};
341    /// # use std::sync::Arc;
342    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
343    /// let mouse = page.mouse();
344    /// let mut options = MouseClickOptions::default();
345    /// options.button = MouseButton::Right;
346    /// mouse.click(300.0, 200.0, options).await?;
347    /// # Ok(())
348    /// # }
349    /// ```
350    pub async fn click(&self, x: f64, y: f64, options: MouseClickOptions) -> Result<()> {
351        self.press(x, y, options.button.clone(), 1).await?;
352        if let Some(delay) = options.delay_after_press {
353            tokio::time::sleep(delay).await;
354        }
355        self.release(x, y, options.button, 1).await
356    }
357
358    /// Convenience helper for a left-button click.
359    pub async fn left_click(&self, x: f64, y: f64) -> Result<()> {
360        self.click(x, y, MouseClickOptions::default()).await
361    }
362
363    /// Convenience helper for a right-button click.
364    pub async fn right_click(&self, x: f64, y: f64) -> Result<()> {
365        let options = MouseClickOptions {
366            button: MouseButton::Right,
367            ..Default::default()
368        };
369        self.click(x, y, options).await
370    }
371
372    /// Performs a double-click; the default delay is 50ms between presses.
373    pub async fn double_click(&self, x: f64, y: f64, options: DoubleClickOptions) -> Result<()> {
374        self.press(x, y, options.button.clone(), 1).await?;
375        self.release(x, y, options.button.clone(), 1).await?;
376        tokio::time::sleep(options.delay_between_clicks).await;
377        self.press(x, y, options.button.clone(), 2).await?;
378        self.release(x, y, options.button, 2).await
379    }
380
381    /// Holds the given button at the coordinates for the requested duration.
382    pub async fn press_and_hold(
383        &self,
384        x: f64,
385        y: f64,
386        button: MouseButton,
387        duration: Duration,
388    ) -> Result<()> {
389        self.press(x, y, button.clone(), 1).await?;
390        tokio::time::sleep(duration).await;
391        self.release(x, y, button, 1).await
392    }
393
394    /// Holds the given button until the condition callback returns `true` or a timeout occurs.
395    ///
396    /// # Examples
397    /// ```no_run
398    /// # use cdp_core::{PressHoldOptions, Page};
399    /// # use std::sync::Arc;
400    /// # use std::time::Duration;
401    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
402    /// let mouse = page.mouse();
403    /// let mut options = PressHoldOptions::default();
404    /// options.timeout = Some(Duration::from_secs(2));
405    /// let success = mouse
406    ///     .press_and_hold_until(400.0, 250.0, options, || async { Ok(true) })
407    ///     .await?;
408    /// assert!(success);
409    /// # Ok(())
410    /// # }
411    /// ```
412    pub async fn press_and_hold_until<F, Fut>(
413        &self,
414        x: f64,
415        y: f64,
416        options: PressHoldOptions,
417        mut condition: F,
418    ) -> Result<bool>
419    where
420        F: FnMut() -> Fut + Send,
421        Fut: Future<Output = Result<bool>> + Send,
422    {
423        self.press(x, y, options.button.clone(), 1).await?;
424
425        if let Some(min_duration) = options.min_duration {
426            tokio::time::sleep(min_duration).await;
427        }
428
429        let start = Instant::now();
430        let mut result = Ok(false);
431
432        loop {
433            if let Some(timeout) = options.timeout
434                && start.elapsed() >= timeout
435            {
436                break;
437            }
438
439            match condition().await {
440                Ok(true) => {
441                    result = Ok(true);
442                    break;
443                }
444                Ok(false) => {}
445                Err(err) => {
446                    result = Err(err);
447                    break;
448                }
449            }
450
451            tokio::time::sleep(options.poll_interval).await;
452        }
453
454        let release_result = self.release(x, y, options.button, 1).await;
455
456        match (result, release_result) {
457            (Err(err), Ok(_)) => Err(err),
458            (_, Err(release_err)) => Err(release_err),
459            (Ok(val), Ok(_)) => Ok(val),
460        }
461    }
462
463    /// Simulates drag gestures by generating a jittered, eased trajectory between two points.
464    ///
465    /// # Examples
466    /// ```no_run
467    /// # use cdp_core::{input::mouse::DragOptions, Page};
468    /// # use std::sync::Arc;
469    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
470    /// let mouse = page.mouse();
471    /// mouse.drag_to(20.0, 20.0, 220.0, 260.0, DragOptions::default()).await?;
472    /// # Ok(())
473    /// # }
474    /// ```
475    pub async fn drag_to(
476        &self,
477        start_x: f64,
478        start_y: f64,
479        end_x: f64,
480        end_y: f64,
481        options: DragOptions,
482    ) -> Result<()> {
483        let DragOptions {
484            button,
485            total_duration,
486            steps,
487            jitter_px,
488            hold_before_move,
489            settle_after_move,
490            move_cursor_to_start,
491            easing,
492        } = options;
493
494        if move_cursor_to_start {
495            let _ = self.move_to(start_x, start_y).await?;
496        }
497
498        self.press(start_x, start_y, button.clone(), 1).await?;
499
500        if let Some(delay) = hold_before_move
501            && !delay.is_zero()
502        {
503            tokio::time::sleep(delay).await;
504        }
505
506        let total_steps = steps.max(2);
507        let delta_x = end_x - start_x;
508        let delta_y = end_y - start_y;
509        let step_delay = total_duration.div_f64(total_steps as f64);
510        let button_mask = Self::button_bitmask(&button);
511        let mut rng = rand::rng();
512
513        for step_idx in 1..=total_steps {
514            let progress = step_idx as f64 / total_steps as f64;
515            let eased = easing.evaluate(progress);
516
517            let base_x = start_x + delta_x * eased;
518            let base_y = start_y + delta_y * eased;
519
520            let apply_jitter = jitter_px > 0.0 && step_idx < total_steps;
521            let jitter_x = if apply_jitter {
522                rng.random_range(-jitter_px..=jitter_px)
523            } else {
524                0.0
525            };
526            let jitter_y = if apply_jitter {
527                rng.random_range(-jitter_px..=jitter_px)
528            } else {
529                0.0
530            };
531
532            let params = DispatchMouseEvent {
533                r#type: DispatchMouseEventTypeOption::MouseMoved,
534                x: base_x + jitter_x,
535                y: base_y + jitter_y,
536                modifiers: None,
537                timestamp: None,
538                button: Some(button.clone()),
539                buttons: Some(button_mask),
540                click_count: Some(0),
541                force: None,
542                tangential_pressure: None,
543                tilt_x: None,
544                tilt_y: None,
545                twist: None,
546                delta_x: None,
547                delta_y: None,
548                pointer_type: None,
549            };
550
551            self.dispatch(params).await?;
552
553            if step_idx < total_steps && !step_delay.is_zero() {
554                tokio::time::sleep(step_delay).await;
555            }
556        }
557
558        if let Some(delay) = settle_after_move
559            && !delay.is_zero()
560        {
561            tokio::time::sleep(delay).await;
562        }
563
564        self.release(end_x, end_y, button, 1).await
565    }
566
567    fn button_bitmask(button: &MouseButton) -> u32 {
568        match button {
569            MouseButton::None => 0,
570            MouseButton::Left => 1,
571            MouseButton::Right => 2,
572            MouseButton::Middle => 4,
573            MouseButton::Back => 8,
574            MouseButton::Forward => 16,
575        }
576    }
577}