use actl_core::CtlError;
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE,
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL,
MOUSEINPUT, SendInput,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};
const WHEEL_DELTA: i32 = 120;
fn normalize(x: i32, y: i32) -> (i32, i32) {
unsafe {
let (vx, vy, vw, vh) = (
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN),
);
if vw <= 0 || vh <= 0 {
return (0, 0);
}
let nx = ((x - vx) as f64 * 65535.0 / vw as f64).round() as i32;
let ny = ((y - vy) as f64 * 65535.0 / vh as f64).round() as i32;
(nx.clamp(0, 65535), ny.clamp(0, 65535))
}
}
fn mouse_event(flags: MOUSE_EVENT_FLAGS, nx: i32, ny: i32, data: i32) -> INPUT {
INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: nx,
dy: ny,
mouseData: data as u32,
dwFlags: flags,
time: 0,
dwExtraInfo: actl_core::handoff::INPUT_TAG,
},
},
}
}
fn send_all(inputs: &[INPUT]) -> Result<(), CtlError> {
crate::feedback::check_stop()?;
send_raw(inputs)
}
fn send_raw(inputs: &[INPUT]) -> Result<(), CtlError> {
let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
if sent == inputs.len() as u32 {
Ok(())
} else {
Err(CtlError::internal(format!(
"SendInput(mouse) delivered {sent}/{} events; pointer state is now unknown",
inputs.len()
)))
}
}
fn abs(flags: MOUSE_EVENT_FLAGS) -> MOUSE_EVENT_FLAGS {
flags | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
}
pub fn left_click_at(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
])
}
pub fn right_click_at(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_RIGHTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_RIGHTUP, nx, ny, 0),
])
}
pub fn double_click_at(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
])
}
pub fn move_to(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0)])
}
pub fn cursor_pos() -> Option<(i32, i32)> {
use windows::Win32::Foundation::POINT;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
let mut pt = POINT::default();
unsafe { GetCursorPos(&mut pt) }.ok().map(|_| (pt.x, pt.y))
}
pub fn drag_to(x1: i32, y1: i32, x2: i32, y2: i32) -> Result<(), CtlError> {
let (ax, ay) = normalize(x1, y1);
let (bx, by) = normalize(x2, y2);
let result = send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), ax, ay, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), ax, ay, 0),
mouse_event(abs(MOUSEEVENTF_MOVE), bx, by, 0),
mouse_event(MOUSEEVENTF_LEFTUP, bx, by, 0),
]);
if result.is_err() {
let _ = send_raw(&[mouse_event(MOUSEEVENTF_LEFTUP, bx, by, 0)]);
}
result
}
const PATH_STEP_PX: i32 = 8;
pub(crate) fn interpolate_path(points: &[(i32, i32)]) -> Vec<(i32, i32)> {
let mut out = Vec::new();
let Some(&first) = points.first() else {
return out;
};
out.push(first);
for i in 1..points.len() {
let (a, b) = (points[i - 1], points[i]);
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let dist = (dx as f64).hypot(dy as f64).ceil() as i32;
let steps = (dist / PATH_STEP_PX).max(1);
for s in 1..=steps {
let t = s as f64 / steps as f64;
out.push((
a.0 + (dx as f64 * t).round() as i32,
a.1 + (dy as f64 * t).round() as i32,
));
}
}
out
}
pub fn drag_path(points: &[(i32, i32)]) -> Result<(), CtlError> {
let Some(&(sx, sy)) = points.first() else {
return Err(CtlError::protocol("drag path requires at least one point"));
};
let Some(&(ex, ey)) = points.last() else {
return Err(CtlError::protocol("drag path requires at least one point"));
};
let interpolated = interpolate_path(points);
if interpolated.len() > 32_768 {
return Err(CtlError::protocol(format!(
"drag path interpolates to {} move events (max 32768); reduce waypoints or span",
interpolated.len()
)));
}
let (nsx, nsy) = normalize(sx, sy);
let mut events = vec![
mouse_event(abs(MOUSEEVENTF_MOVE), nsx, nsy, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nsx, nsy, 0),
];
for (x, y) in &interpolated {
let (nx, ny) = normalize(*x, *y);
events.push(mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0));
}
let (nex, ney) = normalize(ex, ey);
events.push(mouse_event(MOUSEEVENTF_LEFTUP, nex, ney, 0));
let result = send_all(&events);
if result.is_err() {
let _ = send_raw(&[mouse_event(MOUSEEVENTF_LEFTUP, nex, ney, 0)]);
}
result
}
pub fn wheel_at(x: i32, y: i32, notches: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(
MOUSEEVENTF_WHEEL,
nx,
ny,
notches.saturating_mul(WHEEL_DELTA),
),
])
}
pub fn middle_click_at(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_MIDDLEDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_MIDDLEUP, nx, ny, 0),
])
}
pub fn triple_click_at(x: i32, y: i32) -> Result<(), CtlError> {
let (nx, ny) = normalize(x, y);
let click = || mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0);
let _ = click;
send_all(&[
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
mouse_event(abs(MOUSEEVENTF_LEFTDOWN), nx, ny, 0),
mouse_event(MOUSEEVENTF_LEFTUP, nx, ny, 0),
])
}
pub(crate) fn send_mixed(inputs: Vec<INPUT>) -> Result<(), CtlError> {
send_all(&inputs)
}
pub(crate) fn click_events_for(button: &crate::pointer::MouseButton, x: i32, y: i32) -> Vec<INPUT> {
let (nx, ny) = normalize(x, y);
let (down, up) = match button {
crate::pointer::MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
crate::pointer::MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
crate::pointer::MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
};
vec![
mouse_event(abs(MOUSEEVENTF_MOVE), nx, ny, 0),
mouse_event(abs(down), nx, ny, 0),
mouse_event(up, nx, ny, 0),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interpolate_path_covers_endpoints_and_respects_step() {
let pts = interpolate_path(&[(0, 0), (16, 0), (16, 8)]);
assert_eq!(pts.first(), Some(&(0, 0)));
assert_eq!(pts.last(), Some(&(16, 8)));
assert!(pts.len() >= 4 && pts.len() <= 6, "{pts:?}");
for w in pts.windows(2) {
let d = ((w[1].0 - w[0].0) as f64).hypot((w[1].1 - w[0].1) as f64);
assert!(d <= PATH_STEP_PX as f64 + 1.0, "{w:?}");
}
assert_eq!(interpolate_path(&[]), Vec::<(i32, i32)>::new());
assert_eq!(interpolate_path(&[(5, 5)]), vec![(5, 5)]);
}
#[test]
fn normalize_maps_primary_origin_to_zero() {
unsafe {
let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
let (nx, ny) = normalize(vx, vy);
assert_eq!((nx, ny), (0, 0));
let (fx, fy) = normalize(
vx + GetSystemMetrics(SM_CXVIRTUALSCREEN),
vy + GetSystemMetrics(SM_CYVIRTUALSCREEN),
);
assert!((fx - 65535).abs() <= 1 && (fy - 65535).abs() <= 1);
}
}
}