viewpoint_core/page/mouse/
mod.rs

1//! Mouse input handling.
2//!
3//! Provides direct mouse control for simulating clicks, movement, and scrolling.
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use tokio::sync::Mutex;
9use tracing::{debug, instrument};
10use viewpoint_cdp::CdpConnection;
11use viewpoint_cdp::protocol::input::{
12    DispatchMouseEventParams, DispatchMouseWheelParams, MouseButton, MouseEventType,
13};
14
15use crate::error::LocatorError;
16
17/// Mouse state tracking.
18#[derive(Debug)]
19struct MouseState {
20    /// Current X position.
21    x: f64,
22    /// Current Y position.
23    y: f64,
24    /// Currently pressed button.
25    button: Option<MouseButton>,
26}
27
28impl MouseState {
29    fn new() -> Self {
30        Self {
31            x: 0.0,
32            y: 0.0,
33            button: None,
34        }
35    }
36}
37
38/// Mouse controller for direct mouse input.
39///
40/// Provides methods for moving the mouse, clicking, and scrolling.
41/// All coordinates are in CSS pixels relative to the viewport.
42///
43/// # Example
44///
45/// ```
46/// # #[cfg(feature = "integration")]
47/// # tokio_test::block_on(async {
48/// # use viewpoint_core::Browser;
49/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
50/// # let context = browser.new_context().await.unwrap();
51/// # let page = context.new_page().await.unwrap();
52/// # page.goto("about:blank").goto().await.unwrap();
53///
54/// // Move mouse to coordinates
55/// page.mouse().move_(100.0, 200.0).send().await.unwrap();
56///
57/// // Click at coordinates
58/// page.mouse().click(100.0, 200.0).send().await.unwrap();
59///
60/// // Scroll
61/// page.mouse().wheel(0.0, 100.0).await.unwrap();
62///
63/// // Drag operation
64/// page.mouse().move_(100.0, 100.0).send().await.unwrap();
65/// page.mouse().down().send().await.unwrap();
66/// page.mouse().move_(200.0, 200.0).steps(10).send().await.unwrap();
67/// page.mouse().up().send().await.unwrap();
68/// # });
69/// ```
70#[derive(Debug)]
71pub struct Mouse {
72    /// CDP connection.
73    connection: Arc<CdpConnection>,
74    /// Session ID for the page.
75    session_id: String,
76    /// Mouse state.
77    state: Mutex<MouseState>,
78}
79
80impl Mouse {
81    /// Create a new mouse controller.
82    pub(crate) fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
83        Self {
84            connection,
85            session_id,
86            state: Mutex::new(MouseState::new()),
87        }
88    }
89
90    /// Move the mouse to the specified coordinates.
91    ///
92    /// Returns a builder for additional options.
93    pub fn move_(&self, x: f64, y: f64) -> MoveBuilder<'_> {
94        MoveBuilder {
95            mouse: self,
96            x,
97            y,
98            steps: 1,
99        }
100    }
101
102    /// Click at the specified coordinates.
103    ///
104    /// Returns a builder for additional options.
105    pub fn click(&self, x: f64, y: f64) -> ClickBuilder<'_> {
106        ClickBuilder {
107            mouse: self,
108            x,
109            y,
110            button: MouseButton::Left,
111            click_count: 1,
112            delay: None,
113        }
114    }
115
116    /// Double-click at the specified coordinates.
117    #[instrument(level = "debug", skip(self), fields(x = x, y = y))]
118    pub async fn dblclick(&self, x: f64, y: f64) -> Result<(), LocatorError> {
119        debug!("Double-clicking at ({}, {})", x, y);
120
121        // First click
122        self.move_(x, y).send().await?;
123        self.down_internal(MouseButton::Left, 1).await?;
124        self.up_internal(MouseButton::Left, 1).await?;
125
126        // Second click
127        self.down_internal(MouseButton::Left, 2).await?;
128        self.up_internal(MouseButton::Left, 2).await?;
129
130        Ok(())
131    }
132
133    /// Press the mouse button at the current position.
134    ///
135    /// Returns a builder for additional options.
136    pub fn down(&self) -> DownBuilder<'_> {
137        DownBuilder {
138            mouse: self,
139            button: MouseButton::Left,
140            click_count: 1,
141        }
142    }
143
144    /// Release the mouse button at the current position.
145    ///
146    /// Returns a builder for additional options.
147    pub fn up(&self) -> UpBuilder<'_> {
148        UpBuilder {
149            mouse: self,
150            button: MouseButton::Left,
151            click_count: 1,
152        }
153    }
154
155    /// Scroll the mouse wheel.
156    #[instrument(level = "debug", skip(self), fields(delta_x = delta_x, delta_y = delta_y))]
157    pub async fn wheel(&self, delta_x: f64, delta_y: f64) -> Result<(), LocatorError> {
158        let state = self.state.lock().await;
159        let x = state.x;
160        let y = state.y;
161        drop(state);
162
163        debug!(
164            "Mouse wheel at ({}, {}): delta=({}, {})",
165            x, y, delta_x, delta_y
166        );
167
168        let params = DispatchMouseWheelParams {
169            event_type: MouseEventType::MouseWheel,
170            x,
171            y,
172            delta_x,
173            delta_y,
174            modifiers: None,
175            pointer_type: None,
176        };
177
178        self.connection
179            .send_command::<_, serde_json::Value>(
180                "Input.dispatchMouseEvent",
181                Some(params),
182                Some(&self.session_id),
183            )
184            .await?;
185
186        Ok(())
187    }
188
189    /// Internal move implementation.
190    async fn move_internal(&self, x: f64, y: f64, steps: u32) -> Result<(), LocatorError> {
191        let (start_x, start_y) = {
192            let state = self.state.lock().await;
193            (state.x, state.y)
194        };
195
196        if steps <= 1 {
197            // Single move
198            self.dispatch_move(x, y).await?;
199        } else {
200            // Move in steps for smooth animation
201            for i in 1..=steps {
202                let progress = f64::from(i) / f64::from(steps);
203                let current_x = start_x + (x - start_x) * progress;
204                let current_y = start_y + (y - start_y) * progress;
205                self.dispatch_move(current_x, current_y).await?;
206            }
207        }
208
209        // Update state
210        {
211            let mut state = self.state.lock().await;
212            state.x = x;
213            state.y = y;
214        }
215
216        Ok(())
217    }
218
219    /// Dispatch a mouse move event.
220    async fn dispatch_move(&self, x: f64, y: f64) -> Result<(), LocatorError> {
221        let params = DispatchMouseEventParams::mouse_move(x, y);
222
223        self.connection
224            .send_command::<_, serde_json::Value>(
225                "Input.dispatchMouseEvent",
226                Some(params),
227                Some(&self.session_id),
228            )
229            .await?;
230
231        Ok(())
232    }
233
234    /// Internal down implementation.
235    async fn down_internal(
236        &self,
237        button: MouseButton,
238        click_count: i32,
239    ) -> Result<(), LocatorError> {
240        let (x, y) = {
241            let state = self.state.lock().await;
242            (state.x, state.y)
243        };
244
245        debug!(
246            "Mouse down at ({}, {}), button={:?}, count={}",
247            x, y, button, click_count
248        );
249
250        let mut params = DispatchMouseEventParams::mouse_down(x, y, button);
251        params.click_count = Some(click_count);
252
253        self.connection
254            .send_command::<_, serde_json::Value>(
255                "Input.dispatchMouseEvent",
256                Some(params),
257                Some(&self.session_id),
258            )
259            .await?;
260
261        // Update state
262        {
263            let mut state = self.state.lock().await;
264            state.button = Some(button);
265        }
266
267        Ok(())
268    }
269
270    /// Internal up implementation.
271    async fn up_internal(&self, button: MouseButton, click_count: i32) -> Result<(), LocatorError> {
272        let (x, y) = {
273            let state = self.state.lock().await;
274            (state.x, state.y)
275        };
276
277        debug!(
278            "Mouse up at ({}, {}), button={:?}, count={}",
279            x, y, button, click_count
280        );
281
282        let mut params = DispatchMouseEventParams::mouse_up(x, y, button);
283        params.click_count = Some(click_count);
284
285        self.connection
286            .send_command::<_, serde_json::Value>(
287                "Input.dispatchMouseEvent",
288                Some(params),
289                Some(&self.session_id),
290            )
291            .await?;
292
293        // Update state
294        {
295            let mut state = self.state.lock().await;
296            state.button = None;
297        }
298
299        Ok(())
300    }
301}
302
303/// Builder for mouse move operations.
304#[derive(Debug)]
305pub struct MoveBuilder<'a> {
306    mouse: &'a Mouse,
307    x: f64,
308    y: f64,
309    steps: u32,
310}
311
312impl MoveBuilder<'_> {
313    /// Set the number of intermediate steps for smooth movement.
314    ///
315    /// Default is 1 (instant move).
316    #[must_use]
317    pub fn steps(mut self, steps: u32) -> Self {
318        self.steps = steps.max(1);
319        self
320    }
321
322    /// Execute the move.
323    #[instrument(level = "debug", skip(self), fields(x = self.x, y = self.y, steps = self.steps))]
324    pub async fn send(self) -> Result<(), LocatorError> {
325        debug!(
326            "Moving mouse to ({}, {}) in {} steps",
327            self.x, self.y, self.steps
328        );
329        self.mouse.move_internal(self.x, self.y, self.steps).await
330    }
331}
332
333/// Builder for mouse click operations.
334#[derive(Debug)]
335pub struct ClickBuilder<'a> {
336    mouse: &'a Mouse,
337    x: f64,
338    y: f64,
339    button: MouseButton,
340    click_count: i32,
341    delay: Option<Duration>,
342}
343
344impl ClickBuilder<'_> {
345    /// Set the mouse button to click.
346    ///
347    /// Default is left button.
348    #[must_use]
349    pub fn button(mut self, button: MouseButton) -> Self {
350        self.button = button;
351        self
352    }
353
354    /// Set the click count (for multi-click).
355    ///
356    /// Default is 1.
357    #[must_use]
358    pub fn click_count(mut self, count: i32) -> Self {
359        self.click_count = count;
360        self
361    }
362
363    /// Set the delay between mouse down and up.
364    #[must_use]
365    pub fn delay(mut self, delay: Duration) -> Self {
366        self.delay = Some(delay);
367        self
368    }
369
370    /// Execute the click.
371    #[instrument(level = "debug", skip(self), fields(x = self.x, y = self.y, button = ?self.button))]
372    pub async fn send(self) -> Result<(), LocatorError> {
373        debug!(
374            "Clicking at ({}, {}), button={:?}",
375            self.x, self.y, self.button
376        );
377
378        // Move to position
379        self.mouse.move_(self.x, self.y).send().await?;
380
381        // Click
382        self.mouse
383            .down_internal(self.button, self.click_count)
384            .await?;
385
386        if let Some(delay) = self.delay {
387            tokio::time::sleep(delay).await;
388        }
389
390        self.mouse
391            .up_internal(self.button, self.click_count)
392            .await?;
393
394        Ok(())
395    }
396}
397
398/// Builder for mouse down operations.
399#[derive(Debug)]
400pub struct DownBuilder<'a> {
401    mouse: &'a Mouse,
402    button: MouseButton,
403    click_count: i32,
404}
405
406impl DownBuilder<'_> {
407    /// Set the mouse button.
408    #[must_use]
409    pub fn button(mut self, button: MouseButton) -> Self {
410        self.button = button;
411        self
412    }
413
414    /// Set the click count.
415    #[must_use]
416    pub fn click_count(mut self, count: i32) -> Self {
417        self.click_count = count;
418        self
419    }
420
421    /// Execute the mouse down.
422    #[instrument(level = "debug", skip(self), fields(button = ?self.button))]
423    pub async fn send(self) -> Result<(), LocatorError> {
424        self.mouse
425            .down_internal(self.button, self.click_count)
426            .await
427    }
428}
429
430/// Builder for mouse up operations.
431#[derive(Debug)]
432pub struct UpBuilder<'a> {
433    mouse: &'a Mouse,
434    button: MouseButton,
435    click_count: i32,
436}
437
438impl UpBuilder<'_> {
439    /// Set the mouse button.
440    #[must_use]
441    pub fn button(mut self, button: MouseButton) -> Self {
442        self.button = button;
443        self
444    }
445
446    /// Set the click count.
447    #[must_use]
448    pub fn click_count(mut self, count: i32) -> Self {
449        self.click_count = count;
450        self
451    }
452
453    /// Execute the mouse up.
454    #[instrument(level = "debug", skip(self), fields(button = ?self.button))]
455    pub async fn send(self) -> Result<(), LocatorError> {
456        self.mouse.up_internal(self.button, self.click_count).await
457    }
458}