use std::time::Duration;
use objc2::rc::Retained;
use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventType, NSWindow};
use objc2_foundation::NSPoint;
use objc2_web_kit::WKWebView;
use crate::engine::{EngineError, EngineResult};
use super::eval::{eval_js_string, run_loop_until};
#[derive(Debug, Clone, Copy)]
pub(super) struct ClientRect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
pub(super) fn ref_rect(
web_view: &Retained<WKWebView>,
r: vs_protocol::Ref,
) -> EngineResult<Option<ClientRect>> {
let js = format!(
r#"(function() {{
var el = document.querySelector('[data-vs-ref="{r}"]');
if (!el) return 'null';
// Scroll into the viewport's vertical center if it's
// off-screen. `instant` keeps the test deterministic
// (no smooth-scroll animation racing the rect read).
try {{
el.scrollIntoView({{behavior: 'instant', block: 'center', inline: 'center'}});
}} catch (e) {{
el.scrollIntoView();
}}
var b = el.getBoundingClientRect();
return JSON.stringify({{x: b.x, y: b.y, w: b.width, h: b.height}});
}})()"#,
r = r.0,
);
let result = eval_js_string(web_view, &js, Duration::from_secs(5))?;
let unwrapped = serde_json::from_str::<String>(&result).unwrap_or(result);
if unwrapped == "null" {
return Ok(None);
}
let v: serde_json::Value = serde_json::from_str(&unwrapped)
.map_err(|e| EngineError::Other(format!("ref_rect parse: {e}")))?;
Ok(Some(ClientRect {
x: v["x"].as_f64().unwrap_or(0.0),
y: v["y"].as_f64().unwrap_or(0.0),
width: v["w"].as_f64().unwrap_or(0.0),
height: v["h"].as_f64().unwrap_or(0.0),
}))
}
pub(super) fn click_at_rect(
web_view: &Retained<WKWebView>,
window: &Retained<NSWindow>,
rect: ClientRect,
webview_height: f64,
start: vs_humanize::Point,
mode: vs_humanize::InputMode,
seed: u64,
) -> EngineResult<vs_humanize::Point> {
let cx = rect.x + rect.width / 2.0;
let cy = rect.y + rect.height / 2.0;
let end = vs_humanize::Point { x: cx, y: cy };
let window_number = window.windowNumber();
let make_event = |ty: NSEventType, p: vs_humanize::Point| -> EngineResult<Retained<NSEvent>> {
let loc = NSPoint::new(p.x, webview_height - p.y);
NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
ty,
loc,
NSEventModifierFlags::empty(),
0.0,
window_number,
None,
0,
1,
1.0,
)
.ok_or_else(|| EngineError::Other(format!("NSEvent::mouseEventWithType returned nil for {ty:?}")))
};
let path = vs_humanize::mouse_path(start, end, mode, seed);
let mut prev_ms: u128 = 0;
for step in &path {
if step.kind == vs_humanize::MouseStepKind::Move {
let mv = make_event(NSEventType::MouseMoved, step.point)?;
web_view.mouseMoved(&mv);
let now_ms = step.at.as_millis();
let delta = now_ms.saturating_sub(prev_ms);
if delta > 0 {
let _ = run_loop_until(
|| false,
Duration::from_millis(u64::try_from(delta).unwrap_or(0)),
);
}
prev_ms = now_ms;
}
}
let down = make_event(NSEventType::LeftMouseDown, end)?;
let up = make_event(NSEventType::LeftMouseUp, end)?;
web_view.mouseDown(&down);
let _ = run_loop_until(|| false, Duration::from_millis(15));
web_view.mouseUp(&up);
let _ = run_loop_until(|| false, Duration::from_millis(30));
Ok(end)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn move_along_path(
web_view: &Retained<WKWebView>,
window: &Retained<NSWindow>,
webview_height: f64,
start: vs_humanize::Point,
end: vs_humanize::Point,
mode: vs_humanize::InputMode,
seed: u64,
button_down: bool,
) -> EngineResult<vs_humanize::Point> {
let window_number = window.windowNumber();
let make_event = |ty: NSEventType, p: vs_humanize::Point| -> EngineResult<Retained<NSEvent>> {
let loc = NSPoint::new(p.x, webview_height - p.y);
NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
ty,
loc,
NSEventModifierFlags::empty(),
0.0,
window_number,
None,
0,
1,
if button_down { 1.0 } else { 0.0 },
)
.ok_or_else(|| EngineError::Other(format!("NSEvent::mouseEventWithType returned nil for {ty:?}")))
};
let path = vs_humanize::mouse_path(start, end, mode, seed);
let mut prev_ms: u128 = 0;
let move_type = if button_down {
NSEventType::LeftMouseDragged
} else {
NSEventType::MouseMoved
};
for step in &path {
if step.kind == vs_humanize::MouseStepKind::Move {
let mv = make_event(move_type, step.point)?;
if button_down {
web_view.mouseDragged(&mv);
} else {
web_view.mouseMoved(&mv);
}
let now_ms = step.at.as_millis();
let delta = now_ms.saturating_sub(prev_ms);
if delta > 0 {
let _ = run_loop_until(
|| false,
Duration::from_millis(u64::try_from(delta).unwrap_or(0)),
);
}
prev_ms = now_ms;
}
}
let final_mv = make_event(move_type, end)?;
if button_down {
web_view.mouseDragged(&final_mv);
} else {
web_view.mouseMoved(&final_mv);
}
Ok(end)
}
pub(super) fn click_at_xy(
web_view: &Retained<WKWebView>,
window: &Retained<NSWindow>,
webview_height: f64,
start: vs_humanize::Point,
target: vs_humanize::Point,
mode: vs_humanize::InputMode,
seed: u64,
) -> EngineResult<vs_humanize::Point> {
let landed = move_along_path(
web_view,
window,
webview_height,
start,
target,
mode,
seed,
false,
)?;
let window_number = window.windowNumber();
let loc = NSPoint::new(target.x, webview_height - target.y);
let make = |ty: NSEventType| -> EngineResult<Retained<NSEvent>> {
NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
ty, loc, NSEventModifierFlags::empty(), 0.0, window_number, None, 0, 1, 1.0,
).ok_or_else(|| EngineError::Other(format!("NSEvent::mouseEventWithType returned nil for {ty:?}")))
};
let down = make(NSEventType::LeftMouseDown)?;
web_view.mouseDown(&down);
let _ = run_loop_until(|| false, Duration::from_millis(15));
let up = make(NSEventType::LeftMouseUp)?;
web_view.mouseUp(&up);
let _ = run_loop_until(|| false, Duration::from_millis(30));
Ok(landed)
}
pub(super) fn drag_xy(
web_view: &Retained<WKWebView>,
window: &Retained<NSWindow>,
webview_height: f64,
start: vs_humanize::Point,
target: vs_humanize::Point,
mode: vs_humanize::InputMode,
seed: u64,
) -> EngineResult<vs_humanize::Point> {
let window_number = window.windowNumber();
let make = |ty: NSEventType, p: vs_humanize::Point| -> EngineResult<Retained<NSEvent>> {
let loc = NSPoint::new(p.x, webview_height - p.y);
NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
ty, loc, NSEventModifierFlags::empty(), 0.0, window_number, None, 0, 1, 1.0,
).ok_or_else(|| EngineError::Other(format!("NSEvent::mouseEventWithType returned nil for {ty:?}")))
};
let down = make(NSEventType::LeftMouseDown, start)?;
web_view.mouseDown(&down);
let _ = run_loop_until(|| false, Duration::from_millis(15));
let landed = move_along_path(
web_view,
window,
webview_height,
start,
target,
mode,
seed,
true,
)?;
let up = make(NSEventType::LeftMouseUp, target)?;
web_view.mouseUp(&up);
let _ = run_loop_until(|| false, Duration::from_millis(30));
Ok(landed)
}