use actl_core::target::Target;
use actl_core::{CtlError, ErrorCode};
use uiautomation::UIAutomation;
use crate::locate::{Located, locate};
use crate::window::{find_window, native_hwnd, window_title_of};
use crate::{input, internal, kbd, mouse, timing};
pub enum PointerAction {
LeftClick,
RightClick,
DoubleClick,
Hover,
Drag { to: (i32, i32) },
Wheel { notches: i32 },
}
pub fn pointer_physical(
app: Option<&str>,
target: &Target,
near: Option<&str>,
action: PointerAction,
) -> Result<(Located, (i32, i32), bool), CtlError> {
use windows::Win32::Foundation::POINT;
use windows::Win32::UI::WindowsAndMessaging::{GA_ROOT, GetAncestor, WindowFromPoint};
let loc = locate(app, target, near)?;
let scrolled = ensure_visible(&loc.element);
let Some(point) = loc.element.get_clickable_point().ok().flatten() else {
return Err(CtlError::new(
ErrorCode::NotActionable,
format!(
"{} ({}) exposes no clickable point; refusing to guess a center point",
target.describe(),
loc.role
),
));
};
let (px, py) = (point.get_x(), point.get_y());
let auto = UIAutomation::new().map_err(internal)?;
let walker = auto.create_tree_walker().map_err(internal)?;
let target_title = {
let mut root = loc.element.clone();
while native_hwnd(&root).is_none() {
match walker.get_parent(&root) {
Ok(p) => root = p,
Err(_) => break,
}
}
let title = native_hwnd(&root).and_then(window_title_of);
match title {
Some(t) => t,
None => find_window(&auto, &walker, &loc.window_title)
.ok()
.and_then(|w| native_hwnd(&w))
.and_then(window_title_of)
.ok_or_else(|| {
CtlError::new(
ErrorCode::NotActionable,
"target window has no native handle for hit-checking",
)
})?,
}
};
let root_of = |hwnd: windows::Win32::Foundation::HWND| {
let r = unsafe { GetAncestor(hwnd, GA_ROOT) };
(!r.0.is_null()).then_some(r)
};
let hit = unsafe { WindowFromPoint(POINT { x: px, y: py }) };
let hit_title = (!hit.0.is_null())
.then_some(hit)
.and_then(root_of)
.and_then(window_title_of);
if hit_title.as_deref() != Some(target_title.as_str()) {
return Err(CtlError::new(
ErrorCode::NotActionable,
format!(
"clickable point ({px},{py}) is occupied by another window \
(occluded or minimized); refusing physical pointer action"
),
));
}
let _lock = input::InputLock::acquire(timing().lock_wait_ms)?;
kbd::wait_modifiers_clear()?;
match action {
PointerAction::LeftClick => mouse::left_click_at(px, py)?,
PointerAction::RightClick => mouse::right_click_at(px, py)?,
PointerAction::DoubleClick => mouse::double_click_at(px, py)?,
PointerAction::Hover => mouse::move_to(px, py)?,
PointerAction::Drag { to: (tx, ty) } => mouse::drag_to(px, py, tx, ty)?,
PointerAction::Wheel { notches } => mouse::wheel_at(px, py, notches)?,
}
Ok((loc, (px, py), scrolled))
}
fn ensure_visible(elem: &uiautomation::UIElement) -> bool {
use uiautomation::patterns::UIScrollItemPattern;
if !elem.is_offscreen().unwrap_or(false) {
return false;
}
let scrolled = elem
.get_pattern::<UIScrollItemPattern>()
.and_then(|p| p.scroll_into_view())
.is_ok();
if scrolled {
std::thread::sleep(std::time::Duration::from_millis(timing().poll_ms));
}
scrolled
}
pub fn click_physical(
app: Option<&str>,
target: &Target,
near: Option<&str>,
) -> Result<(Located, (i32, i32), bool), CtlError> {
pointer_physical(app, target, near, PointerAction::LeftClick)
}
pub fn drag_physical(
app: Option<&str>,
from: &Target,
from_near: Option<&str>,
to: &Target,
to_near: Option<&str>,
) -> Result<(Located, (i32, i32), bool), CtlError> {
let (_, (fx, fy), from_scrolled) =
pointer_physical(app, from, from_near, PointerAction::Hover)?;
let to_loc = locate(app, to, to_near)?;
let to_scrolled = ensure_visible(&to_loc.element);
let Some(dest) = to_loc.element.get_clickable_point().ok().flatten() else {
return Err(CtlError::new(
ErrorCode::NotActionable,
format!(
"{} exposes no clickable point for drag destination",
to.describe()
),
));
};
let point = (dest.get_x(), dest.get_y());
let _lock = input::InputLock::acquire(timing().lock_wait_ms)?;
mouse::drag_to(fx, fy, point.0, point.1)?;
Ok((
locate(app, from, from_near)?,
(fx, fy),
from_scrolled || to_scrolled,
))
}
pub fn scroll_element(
app: Option<&str>,
target: &Target,
near: Option<&str>,
notches: i32,
) -> Result<(Located, &'static str), CtlError> {
use uiautomation::patterns::UIScrollPattern;
use uiautomation::types::ScrollAmount;
let loc = locate(app, target, near)?;
if let Ok(scroll) = loc.element.get_pattern::<UIScrollPattern>() {
let amount = if notches >= 0 {
ScrollAmount::SmallIncrement
} else {
ScrollAmount::SmallDecrement
};
let mut ok = true;
for _ in 0..notches.abs() {
if scroll
.scroll(uiautomation::types::ScrollAmount::NoAmount, amount)
.is_err()
{
ok = false;
break;
}
}
if ok {
return Ok((loc, "uia-scroll"));
}
}
let _ = pointer_physical(app, target, near, PointerAction::Wheel { notches })?;
Ok((loc, "physical-wheel"))
}