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#[derive(Debug, Clone)]
24pub struct MouseClickOptions {
25 pub button: MouseButton,
27 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#[derive(Debug, Clone)]
42pub struct DoubleClickOptions {
43 pub button: MouseButton,
45 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#[derive(Debug, Clone)]
60pub struct PressHoldOptions {
61 pub button: MouseButton,
63 pub poll_interval: Duration,
65 pub timeout: Option<Duration>,
67 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#[derive(Debug, Clone, Copy, Default)]
84pub enum DragEasing {
85 Linear,
87 #[default]
89 EaseInOutCubic,
90 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#[derive(Debug, Clone)]
114pub struct DragOptions {
115 pub button: MouseButton,
117 pub total_duration: Duration,
119 pub steps: usize,
121 pub jitter_px: f64,
123 pub hold_before_move: Option<Duration>,
125 pub settle_after_move: Option<Duration>,
127 pub move_cursor_to_start: bool,
129 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#[derive(Clone)]
150pub struct Mouse {
151 session: Arc<Session>,
152 domain_manager: Arc<DomainManager>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq)]
157pub struct MousePosition {
158 pub viewport_x: f64,
160 pub viewport_y: f64,
162 pub screen_x: f64,
164 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 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 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 pub async fn left_click(&self, x: f64, y: f64) -> Result<()> {
360 self.click(x, y, MouseClickOptions::default()).await
361 }
362
363 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 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 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 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 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}