use super::{BrowserSession, access, humanize};
use crate::browser::{
BrowserError, BrowserOutput,
output::Ack,
request::{KeyboardPressRequest, KeyboardTypeRequest, PointerClick},
};
use chromiumoxide::cdp::browser_protocol::input::{
DispatchKeyEventParams, DispatchKeyEventType, DispatchMouseEventParams, DispatchMouseEventType,
MouseButton,
};
use chromiumoxide::page::Page;
static POINTER_X: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static POINTER_Y: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn last_pointer() -> (f64, f64) {
let xb = POINTER_X.load(std::sync::atomic::Ordering::Relaxed);
let yb = POINTER_Y.load(std::sync::atomic::Ordering::Relaxed);
(f64::from_bits(xb), f64::from_bits(yb))
}
fn remember_pointer(x: f64, y: f64) {
POINTER_X.store(x.to_bits(), std::sync::atomic::Ordering::Relaxed);
POINTER_Y.store(y.to_bits(), std::sync::atomic::Ordering::Relaxed);
}
pub(super) async fn human_move_to(page: &Page, x: f64, y: f64) -> Result<(), BrowserError> {
let (fx, fy) = last_pointer();
let dx = x - fx;
let dy = y - fy;
let dist = (dx * dx + dy * dy).sqrt();
let steps = ((dist / 40.0).ceil() as usize).clamp(3, 12);
for i in 1..=steps {
let t = i as f64 / steps as f64;
let eased = 1.0 - (1.0 - t).powi(3);
let (jx, jy) = humanize::jitter_point(fx + dx * eased, fy + dy * eased, 1.5);
page.execute(
DispatchMouseEventParams::builder()
.x(jx)
.y(jy)
.r#type(DispatchMouseEventType::MouseMoved)
.build()
.map_err(BrowserError::OperationFailed)?,
)
.await?;
}
remember_pointer(x, y);
Ok(())
}
pub(super) async fn human_click_at(page: &Page, x: f64, y: f64) -> Result<(), BrowserError> {
human_move_to(page, x, y).await?;
humanize::settle_delay().await;
press_release(page, x, y).await
}
async fn press_release(page: &Page, x: f64, y: f64) -> Result<(), BrowserError> {
page.execute(
DispatchMouseEventParams::builder()
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.r#type(DispatchMouseEventType::MousePressed)
.build()
.map_err(BrowserError::OperationFailed)?,
)
.await?;
humanize::click_hold_delay().await;
page.execute(
DispatchMouseEventParams::builder()
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.r#type(DispatchMouseEventType::MouseReleased)
.build()
.map_err(BrowserError::OperationFailed)?,
)
.await?;
Ok(())
}
pub(super) async fn mouse_click(
session: &BrowserSession,
request: PointerClick,
) -> Result<BrowserOutput, BrowserError> {
let page = access::current_page(session).await?;
let (x, y) = humanize::jitter_point(request.x, request.y, 2.0);
human_click_at(&page, x, y).await?;
Ok(BrowserOutput::Ack(Ack { ok: true }))
}
pub(super) async fn keyboard_type(
session: &BrowserSession,
request: KeyboardTypeRequest,
) -> Result<BrowserOutput, BrowserError> {
let page = access::current_page(session).await?;
for ch in request.text.chars() {
let mut buf = [0u8; 4];
let s = ch.encode_utf8(&mut buf).to_string();
page.execute(
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::Char)
.text(s.clone())
.unmodified_text(s)
.build()
.map_err(BrowserError::OperationFailed)?,
)
.await?;
humanize::keystroke_delay().await;
}
Ok(BrowserOutput::Ack(Ack { ok: true }))
}
pub(super) async fn keyboard_press(
session: &BrowserSession,
request: KeyboardPressRequest,
) -> Result<BrowserOutput, BrowserError> {
let page = access::current_page(session).await?;
let down = key_event(&request.key, DispatchKeyEventType::KeyDown)?;
let up = key_event(&request.key, DispatchKeyEventType::KeyUp)?;
page.execute(down).await?;
page.execute(up).await?;
Ok(BrowserOutput::Ack(Ack { ok: true }))
}
fn key_event(
key: &str,
kind: DispatchKeyEventType,
) -> Result<DispatchKeyEventParams, BrowserError> {
let mut builder = DispatchKeyEventParams::builder().r#type(kind);
let chars: Vec<char> = key.chars().collect();
if chars.len() == 1 {
let c = chars[0];
let s = c.to_string();
builder = builder.key(s.clone()).text(s.clone()).unmodified_text(s);
} else {
let code = match key {
"Enter" | "Return" => Some((13_i64, "Enter")),
"Tab" => Some((9, "Tab")),
"Escape" | "Esc" => Some((27, "Escape")),
"Backspace" => Some((8, "Backspace")),
"Delete" => Some((46, "Delete")),
"ArrowUp" => Some((38, "ArrowUp")),
"ArrowDown" => Some((40, "ArrowDown")),
"ArrowLeft" => Some((37, "ArrowLeft")),
"ArrowRight" => Some((39, "ArrowRight")),
"Home" => Some((36, "Home")),
"End" => Some((35, "End")),
"PageUp" => Some((33, "PageUp")),
"PageDown" => Some((34, "PageDown")),
"Space" | " " => Some((32, "Space")),
_ => None,
};
builder = builder.key(key.to_string());
if let Some((vk, code)) = code {
builder = builder.windows_virtual_key_code(vk).code(code.to_string());
}
}
builder.build().map_err(BrowserError::OperationFailed)
}