use super::*;
use crate::input_helper::build_helper_invocation;
use crate::{ClickOptions, PressOptions, ScrollOptions, TypeOptions, WebViewInputError};
use serde::Deserialize;
use std::time::Duration;
const LEFT_CLICK_SEQUENCE: [(&str, u8); 2] = [("mousePressed", 1), ("mouseReleased", 0)];
#[derive(Debug, Deserialize)]
struct InputHelperElementResult {
ok: bool,
#[serde(default)]
error: Option<String>,
#[serde(default)]
count: usize,
#[serde(default)]
index: usize,
#[serde(default, rename = "centerX")]
center_x: f64,
#[serde(default, rename = "centerY")]
center_y: f64,
#[serde(default, rename = "viewportWidth")]
viewport_width: f64,
#[serde(default, rename = "viewportHeight")]
viewport_height: f64,
#[serde(default)]
visible: bool,
#[serde(default)]
editable: bool,
}
fn special_key(normalized: &str) -> Option<(&'static str, &'static str, u32)> {
match normalized {
"enter" | "return" => Some(("Enter", "Enter", 0x0D)),
"tab" => Some(("Tab", "Tab", 0x09)),
"space" => Some((" ", "Space", 0x20)),
"backspace" | "delete" => Some(("Backspace", "Backspace", 0x08)),
"forwarddelete" => Some(("Delete", "Delete", 0x2E)),
"escape" | "esc" => Some(("Escape", "Escape", 0x1B)),
"home" => Some(("Home", "Home", 0x24)),
"end" => Some(("End", "End", 0x23)),
"pageup" => Some(("PageUp", "PageUp", 0x21)),
"pagedown" => Some(("PageDown", "PageDown", 0x22)),
"arrowleft" | "left" => Some(("ArrowLeft", "ArrowLeft", 0x25)),
"arrowup" | "up" => Some(("ArrowUp", "ArrowUp", 0x26)),
"arrowright" | "right" => Some(("ArrowRight", "ArrowRight", 0x27)),
"arrowdown" | "down" => Some(("ArrowDown", "ArrowDown", 0x28)),
_ => None,
}
}
fn special_key_text(code: &str) -> Option<&'static str> {
match code {
"Enter" => Some("\r"),
"Tab" => Some("\t"),
"Space" => Some(" "),
_ => None,
}
}
fn scroll_delta(center: f64, viewport: f64) -> f64 {
if !center.is_finite() || !viewport.is_finite() || viewport <= 0.0 {
return 0.0;
}
(center - (viewport / 2.0)).clamp(-900.0, 900.0)
}
impl WebViewInner {
fn cdp(
&self,
method: &str,
params: serde_json::Value,
) -> std::result::Result<(), WebViewInputError> {
self.dispatch_cdp_command(method, params)
.map(|_| ())
.map_err(|err| WebViewInputError::Platform(err.to_string()))
}
fn query_helper_element(
&self,
selector: &str,
index: Option<usize>,
) -> std::result::Result<InputHelperElementResult, WebViewInputError> {
let selector_json = serde_json::to_string(selector)
.map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
let index_json = serde_json::to_string(&index)
.map_err(|err| WebViewInputError::Platform(format!("Invalid selector index: {err}")))?;
let expr = format!("window.__LingXiaInput.query_box({selector_json}, {index_json})");
let value = self
.dispatch_eval_command(build_helper_invocation(&expr))
.map_err(WebViewInputError::Script)?;
let result: InputHelperElementResult = serde_json::from_value(value).map_err(|err| {
WebViewInputError::Platform(format!(
"Failed to decode input helper element result: {err}"
))
})?;
if !result.ok {
return Err(WebViewInputError::ElementNotFound(
result.error.unwrap_or_else(|| {
format!(
"selector matched {} element(s), index {} is unavailable: {}",
result.count, result.index, selector
)
}),
));
}
Ok(result)
}
fn focus_helper_element(
&self,
selector: &str,
index: Option<usize>,
) -> std::result::Result<(), WebViewInputError> {
let selector_json = serde_json::to_string(selector)
.map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
let index_json = serde_json::to_string(&index)
.map_err(|err| WebViewInputError::Platform(format!("Invalid selector index: {err}")))?;
let expr = format!("window.__LingXiaInput.focus({selector_json}, {index_json})");
let value = self
.dispatch_eval_command(build_helper_invocation(&expr))
.map_err(WebViewInputError::Script)?;
let result: InputHelperElementResult = serde_json::from_value(value).map_err(|err| {
WebViewInputError::Platform(format!("Failed to decode input focus result: {err}"))
})?;
if result.ok {
Ok(())
} else {
Err(WebViewInputError::ElementNotInteractable(
result
.error
.unwrap_or_else(|| format!("Element cannot be focused: {selector}")),
))
}
}
fn dispatch_mouse_wheel(
&self,
x: f64,
y: f64,
dx: f64,
dy: f64,
) -> std::result::Result<(), WebViewInputError> {
self.cdp(
"Input.dispatchMouseEvent",
serde_json::json!({
"type": "mouseWheel",
"x": x,
"y": y,
"deltaX": dx,
"deltaY": dy,
}),
)
}
async fn ensure_element_visible(
&self,
selector: &str,
index: Option<usize>,
) -> std::result::Result<InputHelperElementResult, WebViewInputError> {
for _ in 0..12 {
let result = self.query_helper_element(selector, index)?;
if result.visible {
return Ok(result);
}
let dx = scroll_delta(result.center_x, result.viewport_width);
let dy = scroll_delta(result.center_y, result.viewport_height);
if dx.abs() < 1.0 && dy.abs() < 1.0 {
break;
}
self.dispatch_mouse_wheel(
result.viewport_width / 2.0,
result.viewport_height / 2.0,
dx,
dy,
)?;
tokio::time::sleep(Duration::from_millis(80)).await;
}
let result = self.query_helper_element(selector, index)?;
if result.visible {
Ok(result)
} else {
Err(WebViewInputError::ElementNotInteractable(format!(
"Element not visible: {selector}"
)))
}
}
fn dispatch_click_at(&self, x: f64, y: f64) -> std::result::Result<(), WebViewInputError> {
for (kind, buttons) in LEFT_CLICK_SEQUENCE {
self.cdp(
"Input.dispatchMouseEvent",
serde_json::json!({
"type": kind,
"x": x,
"y": y,
"button": "left",
"buttons": buttons,
"clickCount": 1,
}),
)?;
}
Ok(())
}
pub(crate) async fn click_inner(
&self,
selector: &str,
options: ClickOptions,
) -> std::result::Result<(), WebViewInputError> {
let result = self.ensure_element_visible(selector, options.index).await?;
self.dispatch_click_at(result.center_x, result.center_y)
}
pub(crate) async fn type_text_inner(
&self,
selector: &str,
text: &str,
options: TypeOptions,
) -> std::result::Result<(), WebViewInputError> {
let result = self.ensure_element_visible(selector, options.index).await?;
if !result.editable {
return Err(WebViewInputError::ElementNotInteractable(format!(
"Element is not editable: {selector}"
)));
}
self.dispatch_click_at(result.center_x, result.center_y)?;
tokio::time::sleep(Duration::from_millis(32)).await;
if options.replace {
let _ = self
.dispatch_eval_command("document.execCommand('selectAll')".to_string())
.map_err(WebViewInputError::Script)?;
if text.is_empty() {
return self.press_inner("backspace", PressOptions::default()).await;
}
}
if !text.is_empty() {
self.cdp("Input.insertText", serde_json::json!({ "text": text }))?;
}
Ok(())
}
pub(crate) async fn press_inner(
&self,
key: &str,
options: PressOptions,
) -> std::result::Result<(), WebViewInputError> {
if let Some(selector) = options.selector.as_deref() {
self.ensure_element_visible(selector, options.index).await?;
self.focus_helper_element(selector, options.index)?;
}
let normalized = key.trim().to_ascii_lowercase();
if let Some((key_name, code, virtual_key)) = special_key(&normalized) {
let mut down = serde_json::json!({
"type": "rawKeyDown",
"key": key_name,
"code": code,
"windowsVirtualKeyCode": virtual_key,
"nativeVirtualKeyCode": virtual_key,
});
if let Some(text) = special_key_text(code) {
down["type"] = serde_json::json!("keyDown");
down["text"] = serde_json::json!(text);
}
self.cdp("Input.dispatchKeyEvent", down)?;
self.cdp(
"Input.dispatchKeyEvent",
serde_json::json!({
"type": "keyUp",
"key": key_name,
"code": code,
"windowsVirtualKeyCode": virtual_key,
"nativeVirtualKeyCode": virtual_key,
}),
)
} else if key.chars().count() == 1 {
self.cdp("Input.insertText", serde_json::json!({ "text": key }))
} else {
Err(WebViewInputError::Unsupported(
"unsupported Windows key name",
))
}
}
pub(crate) async fn scroll_inner(
&self,
dx: f64,
dy: f64,
_options: ScrollOptions,
) -> std::result::Result<(), WebViewInputError> {
let viewport = self
.dispatch_eval_command("({w: window.innerWidth, h: window.innerHeight})".to_string())
.map_err(WebViewInputError::Script)?;
let width = viewport["w"].as_f64().unwrap_or(0.0);
let height = viewport["h"].as_f64().unwrap_or(0.0);
self.dispatch_mouse_wheel(width / 2.0, height / 2.0, dx, dy)
}
pub(crate) async fn scroll_to_inner(
&self,
selector: &str,
_options: ScrollOptions,
) -> std::result::Result<(), WebViewInputError> {
let _ = self.ensure_element_visible(selector, None).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::LEFT_CLICK_SEQUENCE;
#[test]
fn click_release_clears_button_mask() {
assert_eq!(LEFT_CLICK_SEQUENCE[0], ("mousePressed", 1));
assert_eq!(LEFT_CLICK_SEQUENCE[1], ("mouseReleased", 0));
}
}