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::protocol::input::{
11    DispatchMouseEventParams, DispatchMouseWheelParams, MouseButton, MouseEventType,
12};
13use viewpoint_cdp::CdpConnection;
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!("Mouse wheel at ({}, {}): delta=({}, {})", x, y, delta_x, delta_y);
164
165        let params = DispatchMouseWheelParams {
166            event_type: MouseEventType::MouseWheel,
167            x,
168            y,
169            delta_x,
170            delta_y,
171            modifiers: None,
172            pointer_type: None,
173        };
174
175        self.connection
176            .send_command::<_, serde_json::Value>(
177                "Input.dispatchMouseEvent",
178                Some(params),
179                Some(&self.session_id),
180            )
181            .await?;
182
183        Ok(())
184    }
185
186    /// Internal move implementation.
187    async fn move_internal(&self, x: f64, y: f64, steps: u32) -> Result<(), LocatorError> {
188        let (start_x, start_y) = {
189            let state = self.state.lock().await;
190            (state.x, state.y)
191        };
192
193        if steps <= 1 {
194            // Single move
195            self.dispatch_move(x, y).await?;
196        } else {
197            // Move in steps for smooth animation
198            for i in 1..=steps {
199                let progress = f64::from(i) / f64::from(steps);
200                let current_x = start_x + (x - start_x) * progress;
201                let current_y = start_y + (y - start_y) * progress;
202                self.dispatch_move(current_x, current_y).await?;
203            }
204        }
205
206        // Update state
207        {
208            let mut state = self.state.lock().await;
209            state.x = x;
210            state.y = y;
211        }
212
213        Ok(())
214    }
215
216    /// Dispatch a mouse move event.
217    async fn dispatch_move(&self, x: f64, y: f64) -> Result<(), LocatorError> {
218        let params = DispatchMouseEventParams::mouse_move(x, y);
219
220        self.connection
221            .send_command::<_, serde_json::Value>(
222                "Input.dispatchMouseEvent",
223                Some(params),
224                Some(&self.session_id),
225            )
226            .await?;
227
228        Ok(())
229    }
230
231    /// Internal down implementation.
232    async fn down_internal(
233        &self,
234        button: MouseButton,
235        click_count: i32,
236    ) -> Result<(), LocatorError> {
237        let (x, y) = {
238            let state = self.state.lock().await;
239            (state.x, state.y)
240        };
241
242        debug!("Mouse down at ({}, {}), button={:?}, count={}", x, y, button, click_count);
243
244        let mut params = DispatchMouseEventParams::mouse_down(x, y, button);
245        params.click_count = Some(click_count);
246
247        self.connection
248            .send_command::<_, serde_json::Value>(
249                "Input.dispatchMouseEvent",
250                Some(params),
251                Some(&self.session_id),
252            )
253            .await?;
254
255        // Update state
256        {
257            let mut state = self.state.lock().await;
258            state.button = Some(button);
259        }
260
261        Ok(())
262    }
263
264    /// Internal up implementation.
265    async fn up_internal(&self, button: MouseButton, click_count: i32) -> Result<(), LocatorError> {
266        let (x, y) = {
267            let state = self.state.lock().await;
268            (state.x, state.y)
269        };
270
271        debug!("Mouse up at ({}, {}), button={:?}, count={}", x, y, button, click_count);
272
273        let mut params = DispatchMouseEventParams::mouse_up(x, y, button);
274        params.click_count = Some(click_count);
275
276        self.connection
277            .send_command::<_, serde_json::Value>(
278                "Input.dispatchMouseEvent",
279                Some(params),
280                Some(&self.session_id),
281            )
282            .await?;
283
284        // Update state
285        {
286            let mut state = self.state.lock().await;
287            state.button = None;
288        }
289
290        Ok(())
291    }
292}
293
294/// Builder for mouse move operations.
295#[derive(Debug)]
296pub struct MoveBuilder<'a> {
297    mouse: &'a Mouse,
298    x: f64,
299    y: f64,
300    steps: u32,
301}
302
303impl MoveBuilder<'_> {
304    /// Set the number of intermediate steps for smooth movement.
305    ///
306    /// Default is 1 (instant move).
307    #[must_use]
308    pub fn steps(mut self, steps: u32) -> Self {
309        self.steps = steps.max(1);
310        self
311    }
312
313    /// Execute the move.
314    #[instrument(level = "debug", skip(self), fields(x = self.x, y = self.y, steps = self.steps))]
315    pub async fn send(self) -> Result<(), LocatorError> {
316        debug!("Moving mouse to ({}, {}) in {} steps", self.x, self.y, self.steps);
317        self.mouse.move_internal(self.x, self.y, self.steps).await
318    }
319}
320
321/// Builder for mouse click operations.
322#[derive(Debug)]
323pub struct ClickBuilder<'a> {
324    mouse: &'a Mouse,
325    x: f64,
326    y: f64,
327    button: MouseButton,
328    click_count: i32,
329    delay: Option<Duration>,
330}
331
332impl ClickBuilder<'_> {
333    /// Set the mouse button to click.
334    ///
335    /// Default is left button.
336    #[must_use]
337    pub fn button(mut self, button: MouseButton) -> Self {
338        self.button = button;
339        self
340    }
341
342    /// Set the click count (for multi-click).
343    ///
344    /// Default is 1.
345    #[must_use]
346    pub fn click_count(mut self, count: i32) -> Self {
347        self.click_count = count;
348        self
349    }
350
351    /// Set the delay between mouse down and up.
352    #[must_use]
353    pub fn delay(mut self, delay: Duration) -> Self {
354        self.delay = Some(delay);
355        self
356    }
357
358    /// Execute the click.
359    #[instrument(level = "debug", skip(self), fields(x = self.x, y = self.y, button = ?self.button))]
360    pub async fn send(self) -> Result<(), LocatorError> {
361        debug!("Clicking at ({}, {}), button={:?}", self.x, self.y, self.button);
362
363        // Move to position
364        self.mouse.move_(self.x, self.y).send().await?;
365
366        // Click
367        self.mouse.down_internal(self.button, self.click_count).await?;
368
369        if let Some(delay) = self.delay {
370            tokio::time::sleep(delay).await;
371        }
372
373        self.mouse.up_internal(self.button, self.click_count).await?;
374
375        Ok(())
376    }
377}
378
379/// Builder for mouse down operations.
380#[derive(Debug)]
381pub struct DownBuilder<'a> {
382    mouse: &'a Mouse,
383    button: MouseButton,
384    click_count: i32,
385}
386
387impl DownBuilder<'_> {
388    /// Set the mouse button.
389    #[must_use]
390    pub fn button(mut self, button: MouseButton) -> Self {
391        self.button = button;
392        self
393    }
394
395    /// Set the click count.
396    #[must_use]
397    pub fn click_count(mut self, count: i32) -> Self {
398        self.click_count = count;
399        self
400    }
401
402    /// Execute the mouse down.
403    #[instrument(level = "debug", skip(self), fields(button = ?self.button))]
404    pub async fn send(self) -> Result<(), LocatorError> {
405        self.mouse.down_internal(self.button, self.click_count).await
406    }
407}
408
409/// Builder for mouse up operations.
410#[derive(Debug)]
411pub struct UpBuilder<'a> {
412    mouse: &'a Mouse,
413    button: MouseButton,
414    click_count: i32,
415}
416
417impl UpBuilder<'_> {
418    /// Set the mouse button.
419    #[must_use]
420    pub fn button(mut self, button: MouseButton) -> Self {
421        self.button = button;
422        self
423    }
424
425    /// Set the click count.
426    #[must_use]
427    pub fn click_count(mut self, count: i32) -> Self {
428        self.click_count = count;
429        self
430    }
431
432    /// Execute the mouse up.
433    #[instrument(level = "debug", skip(self), fields(button = ?self.button))]
434    pub async fn send(self) -> Result<(), LocatorError> {
435        self.mouse.up_internal(self.button, self.click_count).await
436    }
437}