use rustenium_bidi_definitions::browsing_context::types::BrowsingContext;
use rustenium_cdp_definitions::browser_protocol::input::commands::{
DispatchMouseEvent, DispatchMouseEventType,
};
use rustenium_cdp_definitions::browser_protocol::input::types::MouseButton as CdpMouseButton;
use rustenium_core::WebsocketConnectionTransport;
use rustenium_core::error::{CdpCommandResultError, CommandResultError};
use rustenium_core::session::CdpSession;
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as TokioMutex;
use crate::error::bidi::InputError;
use crate::error::cdp::MouseInputError;
use crate::input::mouse::{
Mouse, MouseButton, MouseClickOptions, MouseMoveOptions, MouseOptions, MouseWheelOptions, Point,
};
const FLAG_LEFT: i64 = 1;
const FLAG_RIGHT: i64 = 1 << 1;
const FLAG_MIDDLE: i64 = 1 << 2;
const FLAG_BACK: i64 = 1 << 3;
const FLAG_FORWARD: i64 = 1 << 4;
struct CdpMouseState {
position: Point,
buttons: i64,
}
#[derive(Clone)]
pub struct CdpMouse {
pub session: Arc<TokioMutex<CdpSession<WebsocketConnectionTransport>>>,
pub modifiers: Arc<Mutex<i64>>,
state: Arc<Mutex<CdpMouseState>>,
}
impl CdpMouse {
pub fn new(
session: Arc<TokioMutex<CdpSession<WebsocketConnectionTransport>>>,
modifiers: Arc<Mutex<i64>>,
) -> Self {
Self {
session,
modifiers,
state: Arc::new(Mutex::new(CdpMouseState {
position: Point { x: 0.0, y: 0.0 },
buttons: 0,
})),
}
}
fn flag(button: MouseButton) -> i64 {
match button {
MouseButton::Left => FLAG_LEFT,
MouseButton::Right => FLAG_RIGHT,
MouseButton::Middle => FLAG_MIDDLE,
MouseButton::Back => FLAG_BACK,
MouseButton::Forward => FLAG_FORWARD,
}
}
fn to_cdp(button: MouseButton) -> CdpMouseButton {
match button {
MouseButton::Left => CdpMouseButton::Left,
MouseButton::Right => CdpMouseButton::Right,
MouseButton::Middle => CdpMouseButton::Middle,
MouseButton::Back => CdpMouseButton::Back,
MouseButton::Forward => CdpMouseButton::Forward,
}
}
fn active_from_flags(flags: i64) -> CdpMouseButton {
if flags & FLAG_LEFT != 0 {
CdpMouseButton::Left
} else if flags & FLAG_RIGHT != 0 {
CdpMouseButton::Right
} else if flags & FLAG_MIDDLE != 0 {
CdpMouseButton::Middle
} else if flags & FLAG_BACK != 0 {
CdpMouseButton::Back
} else if flags & FLAG_FORWARD != 0 {
CdpMouseButton::Forward
} else {
CdpMouseButton::None
}
}
pub async fn move_to(&self, point: Point, steps: usize) -> Result<(), MouseInputError> {
tracing::debug!(x = point.x, y = point.y, steps, "cdp mouse move_to start");
let steps = steps.max(1);
let modifiers = *self.modifiers.lock().unwrap();
let (from_x, from_y, buttons) = {
let s = self.state.lock().unwrap();
(s.position.x, s.position.y, s.buttons)
};
for i in 1..=steps {
let t = i as f64 / steps as f64;
let cx = from_x + (point.x - from_x) * t;
let cy = from_y + (point.y - from_y) * t;
tracing::debug!(step = i, x = cx, y = cy, "cdp mouse move_to step");
let cmd = DispatchMouseEvent::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(cx)
.y(cy)
.button(Self::active_from_flags(buttons))
.buttons(buttons)
.modifiers(modifiers)
.build()
.unwrap();
self.session.lock().await.send(cmd).await.map_err(|e| {
MouseInputError::CommandError(CdpCommandResultError::SessionSendError(e))
})?;
}
{
let mut s = self.state.lock().unwrap();
s.position.x = point.x;
s.position.y = point.y;
}
tracing::debug!(x = point.x, y = point.y, "cdp mouse move_to done");
Ok(())
}
pub async fn down(
&self,
point: Option<Point>,
button: MouseButton,
click_count: i64,
) -> Result<(), MouseInputError> {
tracing::debug!(button = ?button, "cdp mouse down start");
let flag = Self::flag(button);
let modifiers = *self.modifiers.lock().unwrap();
let (x, y, buttons) = {
let mut s = self.state.lock().unwrap();
if s.buttons & flag != 0 {
return Err(MouseInputError::ButtonAlreadyPressed(format!(
"{:?}",
button
)));
}
s.buttons |= flag;
let (x, y) = match point {
Some(p) => (p.x, p.y),
None => (s.position.x, s.position.y),
};
(x, y, s.buttons)
};
let cmd = DispatchMouseEvent::builder()
.r#type(DispatchMouseEventType::MousePressed)
.x(x)
.y(y)
.button(Self::to_cdp(button))
.buttons(buttons)
.modifiers(modifiers)
.click_count(click_count)
.build()
.unwrap();
self.session.lock().await.send(cmd).await.map_err(|e| {
MouseInputError::CommandError(CdpCommandResultError::SessionSendError(e))
})?;
tracing::debug!(button = ?button, "cdp mouse down done");
Ok(())
}
pub async fn up(
&self,
point: Option<Point>,
button: MouseButton,
click_count: i64,
) -> Result<(), MouseInputError> {
tracing::debug!(button = ?button, "cdp mouse up start");
let flag = Self::flag(button);
let modifiers = *self.modifiers.lock().unwrap();
let (x, y, buttons) = {
let mut s = self.state.lock().unwrap();
if s.buttons & flag == 0 {
return Err(MouseInputError::ButtonNotPressed(format!("{:?}", button)));
}
s.buttons &= !flag;
let (x, y) = match point {
Some(p) => (p.x, p.y),
None => (s.position.x, s.position.y),
};
(x, y, s.buttons)
};
let cmd = DispatchMouseEvent::builder()
.r#type(DispatchMouseEventType::MouseReleased)
.x(x)
.y(y)
.button(Self::to_cdp(button))
.buttons(buttons)
.modifiers(modifiers)
.click_count(click_count)
.build()
.unwrap();
self.session.lock().await.send(cmd).await.map_err(|e| {
MouseInputError::CommandError(CdpCommandResultError::SessionSendError(e))
})?;
tracing::debug!(button = ?button, "cdp mouse up done");
Ok(())
}
pub async fn click(
&self,
point: Option<Point>,
options: MouseClickOptions,
) -> Result<(), MouseInputError> {
tracing::debug!(x = point.map(|p| p.x), y = point.map(|p| p.y), count = options.count, "cdp mouse click start");
let button = options.button.unwrap_or(MouseButton::Left);
let count = options.count.unwrap_or(1) as i64;
let delay_ms = options.delay.unwrap_or(0);
for i in 1..count {
tracing::debug!(n = i, "cdp mouse click press");
self.down(point, button, i).await?;
self.up(point, button, i).await?;
}
self.down(point, button, count).await?;
if delay_ms > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
}
self.up(point, button, count).await?;
tracing::debug!("cdp mouse click done");
Ok(())
}
pub async fn wheel(&self, options: MouseWheelOptions) -> Result<(), MouseInputError> {
tracing::debug!(delta_x = options.delta_x, delta_y = options.delta_y, "cdp mouse wheel start");
let modifiers = *self.modifiers.lock().unwrap();
let (x, y, buttons) = {
let s = self.state.lock().unwrap();
(s.position.x, s.position.y, s.buttons)
};
let cmd = DispatchMouseEvent::builder()
.r#type(DispatchMouseEventType::MouseWheel)
.x(x)
.y(y)
.button(CdpMouseButton::None)
.buttons(buttons)
.modifiers(modifiers)
.delta_x(options.delta_x.unwrap_or(0) as f64)
.delta_y(options.delta_y.unwrap_or(0) as f64)
.build()
.unwrap();
self.session.lock().await.send(cmd).await.map_err(|e| {
MouseInputError::CommandError(CdpCommandResultError::SessionSendError(e))
})?;
tracing::debug!("cdp mouse wheel done");
Ok(())
}
pub async fn reset(&self) -> Result<(), MouseInputError> {
tracing::debug!("cdp mouse reset start");
let held: Vec<MouseButton> = {
let s = self.state.lock().unwrap();
[
(FLAG_LEFT, MouseButton::Left),
(FLAG_RIGHT, MouseButton::Right),
(FLAG_MIDDLE, MouseButton::Middle),
(FLAG_BACK, MouseButton::Back),
(FLAG_FORWARD, MouseButton::Forward),
]
.iter()
.filter(|(flag, _)| s.buttons & flag != 0)
.map(|(_, btn)| *btn)
.collect()
};
for btn in held {
tracing::debug!(button = ?btn, "cdp mouse reset releasing button");
self.up(None, btn, 1).await?;
}
let pos = self.position();
if pos.x != 0.0 || pos.y != 0.0 {
self.move_to(Point {x: 0.0, y: 0.0}, 0).await?;
}
tracing::debug!("cdp mouse reset done");
Ok(())
}
pub fn position(&self) -> Point {
let s = self.state.lock().unwrap();
s.position
}
}
fn to_input_err(e: impl std::fmt::Display) -> InputError {
InputError::CommandResultError(CommandResultError::InvalidResultTypeError(
serde_json::Value::String(e.to_string()),
))
}
impl Mouse for CdpMouse {
fn get_last_position(&self) -> Point {
self.position()
}
fn set_last_position(&self, point: Point) {
if let Ok(mut state) = self.state.try_lock() {
state.position = point;
}
}
async fn reset(&self, _context: &BrowsingContext) -> Result<(), InputError> {
self.reset().await.map_err(|e| to_input_err(e))
}
async fn move_to(
&self,
point: Point,
_context: &BrowsingContext,
options: MouseMoveOptions,
) -> Result<(), InputError> {
let steps = options.steps.unwrap_or(1).max(1);
self.move_to(point, steps)
.await
.map_err(|e| to_input_err(e))
}
async fn down(
&self,
_context: &BrowsingContext,
options: MouseOptions,
) -> Result<(), InputError> {
let button = options.button.unwrap_or(MouseButton::Left);
self.down(None, button, 1).await.map_err(|e| to_input_err(e))
}
async fn up(
&self,
_context: &BrowsingContext,
options: MouseOptions,
) -> Result<(), InputError> {
let button = options.button.unwrap_or(MouseButton::Left);
self.up(None, button, 1).await.map_err(|e| to_input_err(e))
}
async fn click(
&self,
point: Option<Point>,
_context: &BrowsingContext,
options: MouseClickOptions,
) -> Result<(), InputError> {
self.click(point, options).await.map_err(|e| to_input_err(e))
}
async fn wheel(
&self,
_context: &BrowsingContext,
options: MouseWheelOptions,
) -> Result<(), InputError> {
self.wheel(options).await.map_err(|e| to_input_err(e))
}
}