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