use super::super::core::{
self, Impact, Modifier, MouseButton, RawInput, ScrollDirection, SurfaceGeometry, TargetSpec,
};
use super::window;
use anyhow::anyhow;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT, SendInput, VIRTUAL_KEY, VK_A, VK_BACK,
VK_CAPITAL, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_HOME, VK_INSERT,
VK_LEFT, VK_LWIN, VK_MENU, VK_NEXT, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6,
VK_OEM_7, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_PRIOR, VK_RETURN,
VK_RIGHT, VK_SHIFT, VK_SPACE, VK_TAB, VK_UP,
};
use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, SetCursorPos};
const DRAG_STEPS: usize = 12;
const DRAG_STEP_PAUSE: std::time::Duration = std::time::Duration::from_millis(2);
const INPUT_BATCH: usize = 256;
const WHEEL_DELTA: i32 = 120;
const KNOWN_KEYS: &str = "return/enter, tab, space, delete/backspace, forwarddelete, escape/esc, \
home, end, pageup/pagedown, up/down/left/right, f1..f24, capslock, insert, minus, equal, \
comma, period, slash, semicolon, quote, backslash, backtick, leftbracket, rightbracket, \
win/windows/super/meta (a plain Windows-key tap), a-z, 0-9";
pub(super) fn raw_input(target: &TargetSpec, input: RawInput) -> anyhow::Result<Impact> {
let pinned = core::pinned_input(target, &input);
match input {
RawInput::Click {
point,
button,
double,
modifiers,
} => click(target, point, button, double, &modifiers),
RawInput::TypeText { text } => {
let resolved = resolve_window(target)?;
activate_target(&Aim::of(resolved.as_ref(), window::foreground), pinned)?;
send(&type_events(&text))
}
RawInput::KeyChord { chord } => {
let (modifiers, key) = core::parse_key_chord(&chord)?;
let key = vk_for(&key)?;
window::ensure_actuation_allowed(window::foreground())?;
send(&chord_events(&modifiers, key))
}
RawInput::Scroll {
point,
direction,
amount,
} => scroll(target, point, direction, amount, pinned),
RawInput::Drag { from, to } => drag(target, from, to, pinned),
}
}
pub(super) fn cursor_position() -> anyhow::Result<(f64, f64)> {
let mut point = windows::Win32::Foundation::POINT::default();
unsafe { GetCursorPos(&raw mut point) }.map_err(|e| {
core::taxonomy_error(
core::ERR_DEGRADED,
format!("failed to read the pointer position: {e}"),
)
})?;
Ok((f64::from(point.x), f64::from(point.y)))
}
pub(super) fn click_absolute(x: i32, y: i32) -> anyhow::Result<Impact> {
window::ensure_actuation_allowed(window::top_level_at(x, y))?;
set_cursor(x, y)?;
send(&[
mouse_event(0, 0, 0, MOUSEEVENTF_LEFTDOWN),
mouse_event(0, 0, 0, MOUSEEVENTF_LEFTUP),
])
}
pub(super) fn type_text_into_focus(text: &str) -> anyhow::Result<Impact> {
send(&type_events(text))
}
pub(super) fn select_all() -> anyhow::Result<Impact> {
send(&chord_events(&[Modifier::Ctrl], VK_A))
}
enum Aim<'a> {
Window(&'a window::TargetWindow),
Screen(Option<HWND>),
}
impl<'a> Aim<'a> {
fn of(
resolved: Option<&'a window::TargetWindow>,
screen: impl FnOnce() -> Option<HWND>,
) -> Self {
match resolved {
Some(resolved) => Self::Window(resolved),
None => Self::Screen(screen()),
}
}
}
fn activate_target(aim: &Aim<'_>, pinned: bool) -> anyhow::Result<()> {
match aim {
Aim::Window(resolved) => {
window::ensure_actuation_allowed(Some(resolved.hwnd))?;
window::activate(resolved.hwnd);
if pinned {
window::ensure_foreground(resolved.hwnd)?;
}
Ok(())
}
Aim::Screen(landing) => window::ensure_actuation_allowed(*landing),
}
}
fn resolve_window(target: &TargetSpec) -> anyhow::Result<Option<window::TargetWindow>> {
match target {
TargetSpec::Screen => Ok(None),
other => window::resolve(other).map(Some),
}
}
fn surface_of(resolved: Option<&window::TargetWindow>) -> anyhow::Result<SurfaceGeometry> {
match resolved {
Some(resolved) => Ok(resolved.surface),
None => window::screen_geometry(),
}
}
fn click(
target: &TargetSpec,
point: (f64, f64),
button: MouseButton,
double: bool,
modifiers: &[Modifier],
) -> anyhow::Result<Impact> {
let resolved = resolve_window(target)?;
let geometry = surface_of(resolved.as_ref())?;
let (x, y) = core::normalized_to_surface(point.0, point.1, &geometry)?;
let (x, y) = (round_i32(x), round_i32(y));
activate_target(
&Aim::of(resolved.as_ref(), || window::top_level_at(x, y)),
false,
)?;
set_cursor(x, y)?;
let (down, up) = match button {
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
};
let mut events = Vec::with_capacity(modifiers.len() * 2 + 4);
push_modifiers(&mut events, modifiers, false);
for _ in 0..if double { 2 } else { 1 } {
events.push(mouse_event(0, 0, 0, down));
events.push(mouse_event(0, 0, 0, up));
}
push_modifiers(&mut events, modifiers, true);
send(&events)
}
fn scroll(
target: &TargetSpec,
point: Option<(f64, f64)>,
direction: ScrollDirection,
amount: u32,
pinned: bool,
) -> anyhow::Result<Impact> {
let resolved = resolve_window(target)?;
let at = match point {
Some((nx, ny)) => {
let geometry = surface_of(resolved.as_ref())?;
let (x, y) = core::normalized_to_surface(nx, ny, &geometry)?;
Some((round_i32(x), round_i32(y)))
}
None => None,
};
activate_target(
&Aim::of(resolved.as_ref(), || {
at.and_then(|(x, y)| window::top_level_at(x, y))
}),
pinned,
)?;
if let Some((x, y)) = at {
set_cursor(x, y)?;
}
let ticks = i32::try_from(amount.max(1)).unwrap_or(i32::MAX);
let delta = ticks.saturating_mul(WHEEL_DELTA);
let vertical = matches!(direction, ScrollDirection::Up | ScrollDirection::Down);
let sign = match direction {
ScrollDirection::Up | ScrollDirection::Right => 1,
ScrollDirection::Down | ScrollDirection::Left => -1,
};
let data = delta.saturating_mul(sign).cast_unsigned();
let flags = if vertical {
MOUSEEVENTF_WHEEL
} else {
MOUSEEVENTF_HWHEEL
};
send(&[mouse_event(0, 0, data, flags)])
}
fn drag(
target: &TargetSpec,
from: (f64, f64),
to: (f64, f64),
pinned: bool,
) -> anyhow::Result<Impact> {
let resolved = resolve_window(target)?;
let geometry = surface_of(resolved.as_ref())?;
let (fx, fy) = core::normalized_to_surface(from.0, from.1, &geometry)?;
let (tx, ty) = core::normalized_to_surface(to.0, to.1, &geometry)?;
activate_target(
&Aim::of(resolved.as_ref(), || {
window::top_level_at(round_i32(fx), round_i32(fy))
}),
pinned,
)?;
set_cursor(round_i32(fx), round_i32(fy))?;
send(&[mouse_event(0, 0, 0, MOUSEEVENTF_LEFTDOWN)])?;
for step in 1..=DRAG_STEPS {
let progress = step as f64 / DRAG_STEPS as f64;
let x = round_i32(fx + (tx - fx) * progress);
let y = round_i32(fy + (ty - fy) * progress);
if let Err(e) = set_cursor(x, y) {
let released = match send(&[mouse_event(0, 0, 0, MOUSEEVENTF_LEFTUP)]) {
Ok(_) => "the button was released again".to_string(),
Err(err) => format!("the button may still be held down ({err:#})"),
};
return Ok(Impact::Unconfirmed(format!(
"the drag started (the button went down at ({}, {})) but the pointer could not be \
moved further — {e}; {released}",
round_i32(fx),
round_i32(fy)
)));
}
std::thread::sleep(DRAG_STEP_PAUSE);
}
let released = match send(&[mouse_event(0, 0, 0, MOUSEEVENTF_LEFTUP)]) {
Ok(impact) => return Ok(impact),
Err(err) => format!("{err:#}"),
};
Ok(Impact::Unconfirmed(format!(
"the drag ran to its end but the button's release was refused ({released}) — the button \
may still be held down"
)))
}
fn set_cursor(x: i32, y: i32) -> anyhow::Result<()> {
unsafe { SetCursorPos(x, y) }.map_err(|e| {
core::taxonomy_error(
core::ERR_DEGRADED,
format!("failed to move the pointer to ({x}, {y}): {e}"),
)
})
}
fn round_i32(value: f64) -> i32 {
value.round() as i32
}
fn mouse_event(dx: i32, dy: i32, data: u32, flags: MOUSE_EVENT_FLAGS) -> INPUT {
INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx,
dy,
mouseData: data,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
}
}
fn key_event(key: VIRTUAL_KEY, flags: KEYBD_EVENT_FLAGS) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: key,
wScan: 0,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
}
}
fn unicode_event(unit: u16, flags: KEYBD_EVENT_FLAGS) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: VIRTUAL_KEY(0),
wScan: unit,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
}
}
fn push_modifiers(events: &mut Vec<INPUT>, modifiers: &[Modifier], release: bool) {
let ordered: Vec<Modifier> = if release {
modifiers.iter().rev().copied().collect()
} else {
modifiers.to_vec()
};
for modifier in ordered {
let key = modifier_key(modifier);
events.push(key_event(
key,
if release {
KEYEVENTF_KEYUP
} else {
KEYBD_EVENT_FLAGS(0)
},
));
}
}
fn chord_events(modifiers: &[Modifier], key: VIRTUAL_KEY) -> Vec<INPUT> {
let mut events = Vec::with_capacity(modifiers.len() * 2 + 2);
push_modifiers(&mut events, modifiers, false);
events.push(key_event(key, KEYBD_EVENT_FLAGS(0)));
events.push(key_event(key, KEYEVENTF_KEYUP));
push_modifiers(&mut events, modifiers, true);
events
}
fn type_events(text: &str) -> Vec<INPUT> {
let mut events = Vec::with_capacity(text.len() * 2);
let mut previous = '\0';
for character in text.chars() {
let was_carriage_return = previous == '\r';
previous = character;
match character {
'\n' if was_carriage_return => {}
'\n' | '\r' => events.extend(chord_events(&[], VK_RETURN)),
'\t' => events.extend(chord_events(&[], VK_TAB)),
other => {
let mut units = [0u16; 2];
for unit in other.encode_utf16(&mut units) {
events.push(unicode_event(*unit, KEYEVENTF_UNICODE));
events.push(unicode_event(*unit, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP));
}
}
}
}
events
}
fn modifier_key(modifier: Modifier) -> VIRTUAL_KEY {
match modifier {
Modifier::Cmd => VK_LWIN,
Modifier::Ctrl => VK_CONTROL,
Modifier::Alt => VK_MENU,
Modifier::Shift => VK_SHIFT,
}
}
fn vk_for(key: &str) -> anyhow::Result<VIRTUAL_KEY> {
let key = key.trim();
let named = match key {
"return" | "enter" => Some(VK_RETURN),
"tab" => Some(VK_TAB),
"space" => Some(VK_SPACE),
"delete" | "backspace" => Some(VK_BACK),
"forwarddelete" => Some(VK_DELETE),
"escape" | "esc" => Some(VK_ESCAPE),
"home" => Some(VK_HOME),
"end" => Some(VK_END),
"pageup" => Some(VK_PRIOR),
"pagedown" => Some(VK_NEXT),
"up" => Some(VK_UP),
"down" => Some(VK_DOWN),
"left" => Some(VK_LEFT),
"right" => Some(VK_RIGHT),
"capslock" => Some(VK_CAPITAL),
"insert" => Some(VK_INSERT),
"minus" => Some(VK_OEM_MINUS),
"equal" => Some(VK_OEM_PLUS),
"comma" => Some(VK_OEM_COMMA),
"period" => Some(VK_OEM_PERIOD),
"slash" => Some(VK_OEM_2),
"semicolon" => Some(VK_OEM_1),
"quote" => Some(VK_OEM_7),
"backslash" => Some(VK_OEM_5),
"backtick" => Some(VK_OEM_3),
"leftbracket" => Some(VK_OEM_4),
"rightbracket" => Some(VK_OEM_6),
"win" | "windows" | "super" | "meta" => Some(VK_LWIN),
_ => None,
};
if let Some(key) = named {
return Ok(key);
}
let lower = key.to_ascii_lowercase();
if let Some(number) = lower.strip_prefix('f')
&& let Ok(index) = number.parse::<u16>()
&& (1..=24).contains(&index)
{
return Ok(VIRTUAL_KEY(VK_F1.0 + index - 1));
}
if lower.len() == 1 {
let character = lower.as_bytes()[0];
match character {
b'a'..=b'z' => return Ok(VIRTUAL_KEY(u16::from(character - b'a') + 0x41)),
b'0'..=b'9' => return Ok(VIRTUAL_KEY(u16::from(character - b'0') + 0x30)),
_ => {}
}
}
Err(anyhow!(
"unknown key name '{key}' — accepted keys: {KNOWN_KEYS}"
))
}
fn send(events: &[INPUT]) -> anyhow::Result<Impact> {
if events.is_empty() {
return Ok(Impact::Done);
}
for (index, batch) in events.chunks(INPUT_BATCH).enumerate() {
let sent = unsafe { SendInput(batch, std::mem::size_of::<INPUT>() as i32) };
if sent as usize != batch.len() {
let delivered = index * INPUT_BATCH + sent as usize;
if delivered == 0 {
return Err(core::taxonomy_error(
core::ERR_DEGRADED,
"Windows refused the input before any of it was delivered — the foreground \
window may belong to an elevated process, or the secure desktop is up; \
nothing was applied, so bring the window to the foreground and act again",
));
}
let mut detail = format!(
"{delivered} of {} events were delivered and the rest were dropped",
events.len()
);
if let Some(repair) = release_held(&events[..delivered]) {
detail.push_str("; ");
detail.push_str(&repair);
}
return Ok(Impact::Unconfirmed(detail));
}
}
Ok(Impact::Done)
}
fn release_held(landed: &[INPUT]) -> Option<String> {
let mut keys: Vec<VIRTUAL_KEY> = Vec::new();
let mut buttons: Vec<MOUSE_EVENT_FLAGS> = Vec::new();
for input in landed {
if input.r#type == INPUT_KEYBOARD {
let key = unsafe { input.Anonymous.ki };
if key.dwFlags.contains(KEYEVENTF_KEYUP) {
keys.retain(|vk| *vk != key.wVk);
} else if !key.dwFlags.contains(KEYEVENTF_UNICODE) {
keys.push(key.wVk);
}
} else if input.r#type == INPUT_MOUSE {
let flags = unsafe { input.Anonymous.mi.dwFlags };
for (down, up) in [
(MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
(MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
(MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
] {
if flags.contains(down) {
buttons.push(up);
} else if flags.contains(up) {
buttons.retain(|held| *held != up);
}
}
}
}
let mut releases: Vec<INPUT> = buttons
.iter()
.rev()
.map(|up| mouse_event(0, 0, 0, *up))
.collect();
releases.extend(
keys.iter()
.rev()
.map(|key| key_event(*key, KEYEVENTF_KEYUP)),
);
if releases.is_empty() {
return None;
}
let sent = unsafe { SendInput(&releases, std::mem::size_of::<INPUT>() as i32) };
(sent as usize != releases.len()).then(|| {
"the release of what landed was itself refused, so a key or button may still be held down"
.to_string()
})
}