use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
use vs_protocol::{Ref, Tree};
use webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2, ICoreWebView2CompositionController, ICoreWebView2Controller,
ICoreWebView2Environment, ICoreWebView2Environment3,
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG, COREWEBVIEW2_MOUSE_EVENT_KIND,
COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN,
COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP, COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE,
COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE,
};
use webview2_com::{
pwstr_from_str, take_pwstr, AddScriptToExecuteOnDocumentCreatedCompletedHandler,
CapturePreviewCompletedHandler, CreateCoreWebView2CompositionControllerCompletedHandler,
CreateCoreWebView2EnvironmentCompletedHandler, ExecuteScriptCompletedHandler,
NavigationCompletedEventHandler, WebMessageReceivedEventHandler,
};
use windows::core::{Interface, HSTRING, PWSTR};
use windows::Win32::Foundation::{E_POINTER, HWND, POINT, RECT};
use windows::Win32::Graphics::DirectComposition::{
DCompositionCreateDevice2, IDCompositionDevice, IDCompositionTarget, IDCompositionVisual,
};
use windows::Win32::System::Com::{CoInitializeEx, COINIT_APARTMENTTHREADED};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, RegisterClassW, HWND_MESSAGE, WINDOW_EX_STYLE, WNDCLASSW, WS_OVERLAPPED,
};
use crate::backend::inspector_bridge::{
self, InspectorSlots, NetworkIngestSlot, CONSOLE_HANDLER, NETWORK_HANDLER,
};
use crate::engine::{
ActTarget, Action, AuthBlob, CaptureScope, CursorOp, Engine, EngineCapabilities, EngineError,
EngineResult, InputMode, LayoutBox, PageHandle, Viewport, WaitCondition,
};
use super::common::SNAPSHOT_DOM_WALKER_JS as SNAPSHOT_JS;
#[allow(dead_code)]
struct W2Page {
comp_controller: ICoreWebView2CompositionController,
controller: ICoreWebView2Controller,
web_view: ICoreWebView2,
parent_hwnd: HWND,
_dcomp_device: IDCompositionDevice,
_dcomp_target: IDCompositionTarget,
_dcomp_visual: IDCompositionVisual,
inspector: InspectorSlots,
inspector_installed: bool,
cookie_baseline: std::cell::RefCell<Option<Vec<super::auth::CookieData>>>,
cookie_next_seq: std::cell::RefCell<u64>,
last_mouse: std::cell::Cell<vs_humanize::Point>,
}
pub struct Webview2Backend {
pages: HashMap<PageHandle, W2Page>,
next_handle: u64,
captures_dir: Option<PathBuf>,
parent_hwnd: Option<HWND>,
}
impl Default for Webview2Backend {
fn default() -> Self {
Self::new()
}
}
unsafe extern "system" fn wnd_proc(
hwnd: windows::Win32::Foundation::HWND,
msg: u32,
wparam: windows::Win32::Foundation::WPARAM,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::LRESULT {
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
impl Webview2Backend {
#[must_use]
pub fn new() -> Self {
Self {
pages: HashMap::new(),
next_handle: 1,
captures_dir: None,
parent_hwnd: None,
}
}
#[must_use]
pub fn with_capture_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.captures_dir = Some(dir.into());
self
}
fn alloc_handle(&mut self) -> PageHandle {
let h = PageHandle(self.next_handle);
self.next_handle += 1;
h
}
fn page_mut(&mut self, h: PageHandle) -> EngineResult<&mut W2Page> {
self.pages.get_mut(&h).ok_or(EngineError::NotFound {
kind: "page",
id: h.0.to_string(),
})
}
fn create_host_hwnd(&mut self) -> EngineResult<HWND> {
unsafe {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
}
let class_name: HSTRING = HSTRING::from("vs-webview2-host");
let class = WNDCLASSW {
lpfnWndProc: Some(wnd_proc),
lpszClassName: windows::core::PCWSTR(class_name.as_ptr()),
..Default::default()
};
unsafe {
let _atom = RegisterClassW(&raw const class);
}
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR::null(),
WS_OVERLAPPED,
0,
0,
1280,
800,
Some(HWND_MESSAGE),
None,
None,
None,
)
}
.map_err(|e| EngineError::Other(format!("CreateWindowExW: {e}")))?;
self.parent_hwnd = Some(hwnd);
Ok(hwnd)
}
}
fn execute_script(web_view: &ICoreWebView2, js: &str) -> EngineResult<String> {
let (tx, rx) = mpsc::channel();
let js_owned = js.to_string();
let web_view_owned: ICoreWebView2 = web_view.clone();
ExecuteScriptCompletedHandler::wait_for_async_operation(
Box::new(move |handler| {
let pwstr = pwstr_from_str(&js_owned);
unsafe { web_view_owned.ExecuteScript(windows::core::PCWSTR(pwstr.0), &handler) }
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, result_json| {
error_code?;
let _ = tx.send(result_json);
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("ExecuteScript: {e:?}")))?;
let json = rx
.recv()
.map_err(|_| EngineError::Other("ExecuteScript: channel closed".into()))?;
Ok(json)
}
fn add_init_script(web_view: &ICoreWebView2, js: &str) -> EngineResult<String> {
let (tx, rx) = mpsc::channel();
let js_owned = js.to_string();
let web_view_owned: ICoreWebView2 = web_view.clone();
AddScriptToExecuteOnDocumentCreatedCompletedHandler::wait_for_async_operation(
Box::new(move |handler| {
let pwstr = pwstr_from_str(&js_owned);
unsafe {
web_view_owned
.AddScriptToExecuteOnDocumentCreated(windows::core::PCWSTR(pwstr.0), &handler)
}
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, script_id| {
error_code?;
let _ = tx.send(script_id);
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("AddScriptToExecuteOnDocumentCreated: {e:?}")))?;
rx.recv()
.map_err(|_| EngineError::Other("AddScript: channel closed".into()))
}
fn install_inspector(web_view: &ICoreWebView2, slots: &InspectorSlots) -> bool {
if std::env::var_os("VS_DISABLE_INSPECTOR").is_some() {
return false;
}
let cs_console = slots.console.clone();
let cs_network = slots.network.clone();
let cs_details = slots.details.clone();
let cs_pending = slots.pending.clone();
let shim = r"
window.webkit = window.webkit || {};
window.webkit.messageHandlers = window.webkit.messageHandlers || {};
function __vsMakeHandler(name) {
return {
postMessage: function(msg) {
var s = (typeof msg === 'string') ? msg : JSON.stringify(msg);
// Pass an object, not a string. postMessage of a
// string makes WebMessageAsJson on the host side
// return a JSON-encoded string literal that our
// host parser cannot field-access. The object
// form makes WebMessageAsJson return the object,
// and our handler reads __channel + body off it.
window.chrome.webview.postMessage({ __channel: name, body: s });
},
};
}
window.webkit.messageHandlers.vsConsole = __vsMakeHandler('vsConsole');
window.webkit.messageHandlers.vsNetwork = __vsMakeHandler('vsNetwork');
";
if add_init_script(web_view, shim).is_err() {
return false;
}
if add_init_script(web_view, inspector_bridge::SCRIPT).is_err() {
return false;
}
let handler = WebMessageReceivedEventHandler::create(Box::new(move |_sender, args_opt| {
let Some(args) = args_opt else {
return Ok(());
};
let mut raw = PWSTR(std::ptr::null_mut());
if unsafe { args.WebMessageAsJson(&raw mut raw) }.is_err() {
return Ok(());
}
let outer = take_pwstr(raw);
let v: serde_json::Value = match serde_json::from_str(&outer) {
Ok(v) => v,
Err(_) => return Ok(()),
};
let channel = v.get("__channel").and_then(|x| x.as_str()).unwrap_or("");
let body = v.get("body").and_then(|x| x.as_str()).unwrap_or("");
match channel {
CONSOLE_HANDLER => {
let mut buf = cs_console.borrow_mut();
inspector_bridge::ingest_console(&mut buf, body);
}
NETWORK_HANDLER => {
let mut entries = cs_network.borrow_mut();
let mut details = cs_details.borrow_mut();
let mut pending = cs_pending.borrow_mut();
inspector_bridge::ingest_network(
NetworkIngestSlot {
entries: &mut entries,
details: &mut details,
pending: &mut pending,
},
body,
);
}
_ => {}
}
Ok(())
}));
let mut token: i64 = 0;
if unsafe { web_view.add_WebMessageReceived(&handler, &raw mut token) }.is_err() {
return false;
}
true
}
impl Engine for Webview2Backend {
fn open(&mut self, url: &str) -> EngineResult<PageHandle> {
let parent = self.create_host_hwnd()?;
let environment: ICoreWebView2Environment = {
let (tx, rx) = mpsc::channel();
CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation(
Box::new(|handler| unsafe {
webview2_com::Microsoft::Web::WebView2::Win32::CreateCoreWebView2Environment(
&handler,
)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, environment| {
error_code?;
tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER)))
.map_err(|_| windows::core::Error::from(E_POINTER))?;
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("CreateEnvironment: {e:?}")))?;
rx.recv()
.map_err(|_| EngineError::Other("Environment channel closed".into()))?
.map_err(|e| EngineError::Other(format!("Environment: {e}")))?
};
let env3: ICoreWebView2Environment3 = environment
.cast::<ICoreWebView2Environment3>()
.map_err(|e| EngineError::Other(format!("cast Environment3: {e}")))?;
let comp_controller: ICoreWebView2CompositionController = {
let (tx, rx) = mpsc::channel();
let env = env3.clone();
CreateCoreWebView2CompositionControllerCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
env.CreateCoreWebView2CompositionController(parent, &handler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, controller| {
error_code?;
tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER)))
.map_err(|_| windows::core::Error::from(E_POINTER))?;
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("CreateCompositionController: {e:?}")))?;
rx.recv()
.map_err(|_| EngineError::Other("Controller channel closed".into()))?
.map_err(|e| EngineError::Other(format!("Controller: {e}")))?
};
let controller: ICoreWebView2Controller = comp_controller
.cast::<ICoreWebView2Controller>()
.map_err(|e| EngineError::Other(format!("cast Controller: {e}")))?;
let dcomp_device: IDCompositionDevice = unsafe {
DCompositionCreateDevice2::<_, IDCompositionDevice>(None)
.map_err(|e| EngineError::Other(format!("DCompositionCreateDevice2: {e}")))?
};
let dcomp_target: IDCompositionTarget = unsafe {
dcomp_device
.CreateTargetForHwnd(parent, true)
.map_err(|e| EngineError::Other(format!("CreateTargetForHwnd: {e}")))?
};
let dcomp_visual: IDCompositionVisual = unsafe {
dcomp_device
.CreateVisual()
.map_err(|e| EngineError::Other(format!("CreateVisual: {e}")))?
};
unsafe {
dcomp_target
.SetRoot(&dcomp_visual)
.map_err(|e| EngineError::Other(format!("SetRoot: {e}")))?;
comp_controller
.SetRootVisualTarget(&dcomp_visual)
.map_err(|e| EngineError::Other(format!("SetRootVisualTarget: {e}")))?;
dcomp_device
.Commit()
.map_err(|e| EngineError::Other(format!("DComposition Commit: {e}")))?;
}
unsafe {
controller.SetBounds(RECT {
left: 0,
top: 0,
right: 1280,
bottom: 800,
})
}
.map_err(|e| EngineError::Other(format!("SetBounds: {e}")))?;
let web_view: ICoreWebView2 = unsafe { controller.CoreWebView2() }
.map_err(|e| EngineError::Other(format!("CoreWebView2: {e}")))?;
unsafe {
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings2;
use windows::core::Interface;
let settings = web_view
.Settings()
.map_err(|e| EngineError::Other(format!("Settings: {e}")))?;
if let Ok(s2) = settings.cast::<ICoreWebView2Settings2>() {
let ua: Vec<u16> = crate::engine::DEFAULT_USER_AGENT
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let _ = s2.SetUserAgent(windows::core::PCWSTR(ua.as_ptr()));
}
}
let inspector = InspectorSlots::new(crate::inspector::DEFAULT_BUFFER_CAPACITY);
let inspector_installed = install_inspector(&web_view, &inspector);
let (tx, rx) = mpsc::channel();
let handler = NavigationCompletedEventHandler::create(Box::new(move |_sender, _args| {
let _ = tx.send(());
Ok(())
}));
let mut token: i64 = 0;
unsafe { web_view.add_NavigationCompleted(&handler, &raw mut token) }
.map_err(|e| EngineError::Other(format!("add_NavigationCompleted: {e}")))?;
let url_pwstr = pwstr_from_str(url);
unsafe { web_view.Navigate(windows::core::PCWSTR(url_pwstr.0)) }
.map_err(|e| EngineError::Other(format!("Navigate: {e}")))?;
webview2_com::wait_with_pump(rx)
.map_err(|e| EngineError::Other(format!("wait_with_pump: {e:?}")))?;
unsafe { web_view.remove_NavigationCompleted(token) }
.map_err(|e| EngineError::Other(format!("remove_NavigationCompleted: {e}")))?;
let handle = self.alloc_handle();
self.pages.insert(
handle,
W2Page {
comp_controller,
controller,
web_view,
parent_hwnd: parent,
_dcomp_device: dcomp_device,
_dcomp_target: dcomp_target,
_dcomp_visual: dcomp_visual,
inspector,
inspector_installed,
cookie_baseline: std::cell::RefCell::new(None),
cookie_next_seq: std::cell::RefCell::new(0),
last_mouse: std::cell::Cell::new(vs_humanize::Point { x: 0.0, y: 0.0 }),
},
);
Ok(handle)
}
fn close(&mut self, page: PageHandle) -> EngineResult<()> {
if let Some(p) = self.pages.remove(&page) {
let _ = unsafe { p.controller.Close() };
}
Ok(())
}
fn snapshot(&mut self, page: PageHandle) -> EngineResult<Tree> {
let p = self.page_mut(page)?;
let json = execute_script(&p.web_view, SNAPSHOT_JS)?;
super::common::parse_snapshot(&json).map_err(EngineError::Other)
}
fn act(&mut self, page: PageHandle, target: ActTarget, action: Action) -> EngineResult<()> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_act(
move |js, _budget| execute_script(&web_view, js),
&target,
&action,
)
}
fn wait(
&mut self,
page: PageHandle,
cond: WaitCondition,
budget: Duration,
) -> EngineResult<()> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_wait(
|js, _budget| execute_script(&web_view, js),
&cond,
budget,
|| {
use windows::Win32::UI::WindowsAndMessaging::{
DispatchMessageW, PeekMessageW, MSG, PM_REMOVE,
};
let mut msg = MSG::default();
unsafe {
while PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool() {
DispatchMessageW(&raw const msg);
}
}
std::thread::sleep(Duration::from_millis(20));
},
)
}
fn capture(&mut self, page: PageHandle, _scope: CaptureScope) -> EngineResult<PathBuf> {
let captures_dir = self.captures_dir.clone().unwrap_or_else(std::env::temp_dir);
let _ = std::fs::create_dir_all(&captures_dir);
let path = captures_dir.join(format!("capture-{}.png", page.0));
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
let stream = unsafe {
windows::Win32::System::Com::StructuredStorage::CreateStreamOnHGlobal(
windows::Win32::Foundation::HGLOBAL(std::ptr::null_mut()),
true,
)
}
.map_err(|e| EngineError::Other(format!("CreateStreamOnHGlobal: {e}")))?;
let (tx, rx) = mpsc::channel();
let stream_for_handler = stream.clone();
CapturePreviewCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
web_view
.CapturePreview(
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG,
&stream_for_handler,
&handler,
)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code| {
error_code?;
let _ = tx.send(());
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("CapturePreview: {e:?}")))?;
rx.recv()
.map_err(|_| EngineError::Other("CapturePreview channel closed".into()))?;
unsafe {
stream
.Seek(0, windows::Win32::System::Com::STREAM_SEEK_SET, None)
.map_err(|e| EngineError::Other(format!("Stream Seek: {e}")))?;
}
let mut buf = Vec::new();
let mut chunk = [0u8; 4096];
loop {
let mut read = 0u32;
let res = unsafe {
stream.Read(
chunk.as_mut_ptr().cast(),
u32::try_from(chunk.len()).unwrap_or(u32::MAX),
Some(&raw mut read),
)
};
res.ok()
.map_err(|e| EngineError::Other(format!("Stream Read: {e}")))?;
if read == 0 {
break;
}
buf.extend_from_slice(&chunk[..read as usize]);
}
std::fs::write(&path, &buf)
.map_err(|e| EngineError::Other(format!("write capture: {e}")))?;
Ok(path)
}
fn layout(&mut self, page: PageHandle, refs: &[Ref]) -> EngineResult<Vec<LayoutBox>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_layout(move |js, _budget| execute_script(&web_view, js), refs)
}
fn set_viewport(&mut self, page: PageHandle, viewport: Viewport) -> EngineResult<()> {
let p = self.page_mut(page)?;
unsafe {
p.controller.SetBounds(RECT {
left: 0,
top: 0,
right: i32::try_from(viewport.width).unwrap_or(1280),
bottom: i32::try_from(viewport.height).unwrap_or(800),
})
}
.map_err(|e| EngineError::Other(format!("SetBounds: {e}")))?;
Ok(())
}
fn save_auth(&mut self, page: PageHandle) -> EngineResult<AuthBlob> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
let cookies = wv2_cookies::get_all_cookies(&web_view)?;
let storage =
super::common::run_save_storage_only(move |js, _budget| execute_script(&web_view, js))?;
let blob = super::auth::AuthBlobV2 {
version: 2,
url: storage.url,
origin: storage.origin,
cookies,
local_storage: storage.local_storage,
session_storage: storage.session_storage,
};
super::auth::encode(&blob)
}
fn load_auth(&mut self, page: PageHandle, blob: &AuthBlob) -> EngineResult<()> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
let parsed = super::auth::decode(blob)?;
wv2_cookies::set_cookies(&web_view, &parsed.cookies)?;
super::common::run_load_storage_only(
move |js, _budget| execute_script(&web_view, js),
&parsed.local_storage,
&parsed.session_storage,
)
}
fn console_entries(
&mut self,
page: PageHandle,
) -> EngineResult<Vec<crate::inspector::ConsoleEntry>> {
let p = self.page_mut(page)?;
Ok(p.inspector.console.borrow().snapshot())
}
fn network_entries(
&mut self,
page: PageHandle,
) -> EngineResult<Vec<crate::inspector::NetworkEntry>> {
let p = self.page_mut(page)?;
Ok(p.inspector.network.borrow().snapshot())
}
fn request_detail(
&mut self,
page: PageHandle,
seq: u64,
) -> EngineResult<Option<crate::inspector::RequestDetail>> {
let p = self.page_mut(page)?;
Ok(p.inspector.details.borrow().get(seq).cloned())
}
fn eval_js(
&mut self,
page: PageHandle,
expr: &str,
) -> EngineResult<crate::inspector::EvalResult> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_eval(move |js, _budget| execute_script(&web_view, js), expr)
}
fn storage(
&mut self,
page: PageHandle,
scope: crate::inspector::StorageScope,
) -> EngineResult<Vec<crate::inspector::StorageEntry>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
if matches!(scope, crate::inspector::StorageScope::Cookies) {
let cookies = wv2_cookies::get_all_cookies(&web_view)?;
return Ok(cookies
.iter()
.map(super::common::cookie_to_storage_entry)
.collect());
}
super::common::run_storage(move |js, _budget| execute_script(&web_view, js), scope)
}
fn scripts(&mut self, page: PageHandle) -> EngineResult<Vec<crate::inspector::ScriptEntry>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_scripts(move |js, _budget| execute_script(&web_view, js))
}
fn script_source(
&mut self,
page: PageHandle,
seq: u64,
) -> EngineResult<Option<crate::inspector::ScriptSource>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_script_source(move |js, _budget| execute_script(&web_view, js), seq)
}
fn dom(
&mut self,
page: PageHandle,
r: Ref,
extra_props: &[String],
) -> EngineResult<Option<crate::inspector::DomDetail>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_dom(
move |js, _budget| execute_script(&web_view, js),
r,
extra_props,
)
}
fn performance(
&mut self,
page: PageHandle,
) -> EngineResult<crate::inspector::PerformanceMetrics> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
super::common::run_performance(move |js, _budget| execute_script(&web_view, js))
}
fn cookie_events(
&mut self,
page: PageHandle,
) -> EngineResult<Vec<crate::inspector::CookieEvent>> {
let p = self.page_mut(page)?;
let web_view = p.web_view.clone();
let current = wv2_cookies::get_all_cookies(&web_view)?;
let previous = p.cookie_baseline.borrow().clone();
let mut seq = p.cookie_next_seq.borrow_mut();
let events = super::common::diff_cookies(previous.as_deref(), ¤t, &mut seq);
*p.cookie_baseline.borrow_mut() = Some(current);
Ok(events)
}
fn cursor_op(&mut self, page: PageHandle, op: CursorOp, mode: InputMode) -> EngineResult<()> {
let p = self.page_mut(page)?;
let comp = p.comp_controller.clone();
let humanize_mode = match mode {
InputMode::Human => vs_humanize::InputMode::Human,
InputMode::Careful => vs_humanize::InputMode::Careful,
InputMode::Robotic => vs_humanize::InputMode::Robotic,
};
let start = p.last_mouse.get();
let seed = wv2_humanize_seed(op);
let landed = match op {
CursorOp::MoveTo { x, y } | CursorOp::HoverAt { x, y } => wv2_move_along_path(
&comp,
start,
vs_humanize::Point { x, y },
humanize_mode,
seed,
)?,
CursorOp::ClickAt { x, y } => {
let landed = wv2_move_along_path(
&comp,
start,
vs_humanize::Point { x, y },
humanize_mode,
seed,
)?;
wv2_press_release(&comp, landed)?;
landed
}
CursorOp::Drag { x1, y1, x2, y2 } => {
let start_pt = vs_humanize::Point { x: x1, y: y1 };
let target = vs_humanize::Point { x: x2, y: y2 };
let pre = wv2_move_along_path(&comp, start, start_pt, humanize_mode, seed)?;
wv2_send_mouse(
&comp,
COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN,
pre,
)?;
std::thread::sleep(Duration::from_millis(15));
let landed = wv2_move_along_path(&comp, pre, target, humanize_mode, seed)?;
wv2_send_mouse(
&comp,
COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP,
landed,
)?;
let html5_js = super::common::build_html5_drag_js(x1, y1, x2, y2);
let web_view = p.web_view.clone();
let _ = execute_script(&web_view, &html5_js);
landed
}
};
p.last_mouse.set(landed);
Ok(())
}
fn capabilities(&self) -> EngineCapabilities {
let any_inspector = self.pages.values().any(|p| p.inspector_installed);
EngineCapabilities {
renders: true,
honors_viewport: true,
measures_layout: true,
persists_auth: true,
inspector_console: any_inspector,
inspector_network: any_inspector,
inspector_cookie_events: true,
name: "webview2",
version: "Windows WebView2 (webview2-com 0.39)",
}
}
}
fn wv2_humanize_seed(op: CursorOp) -> u64 {
let (a, b, c, d) = match op {
CursorOp::MoveTo { x, y } | CursorOp::HoverAt { x, y } | CursorOp::ClickAt { x, y } => {
(x, y, 0.0, 0.0)
}
CursorOp::Drag { x1, y1, x2, y2 } => (x1, y1, x2, y2),
};
let bits = |v: f64| v.to_bits();
bits(a).wrapping_mul(0x9E37_79B9_7F4A_7C15)
^ bits(b).wrapping_mul(0xBF58_476D_1CE4_E5B9)
^ bits(c).wrapping_mul(0x94D0_49BB_1331_11EB)
^ bits(d)
}
#[allow(clippy::cast_possible_truncation)]
fn point_at(p: vs_humanize::Point) -> POINT {
POINT {
x: p.x.round() as i32,
y: p.y.round() as i32,
}
}
fn wv2_send_mouse(
comp: &ICoreWebView2CompositionController,
kind: COREWEBVIEW2_MOUSE_EVENT_KIND,
point: vs_humanize::Point,
) -> EngineResult<()> {
unsafe {
comp.SendMouseInput(kind, COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, 0, point_at(point))
.map_err(|e| EngineError::Other(format!("SendMouseInput: {e}")))
}
}
fn wv2_move_along_path(
comp: &ICoreWebView2CompositionController,
start: vs_humanize::Point,
end: vs_humanize::Point,
mode: vs_humanize::InputMode,
seed: u64,
) -> EngineResult<vs_humanize::Point> {
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 {
continue;
}
wv2_send_mouse(comp, COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, step.point)?;
let dt = step.at.as_millis().saturating_sub(prev_ms);
if dt > 0 {
std::thread::sleep(Duration::from_millis(u64::try_from(dt).unwrap_or(0)));
}
prev_ms = step.at.as_millis();
}
wv2_send_mouse(comp, COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, end)?;
Ok(end)
}
fn wv2_press_release(
comp: &ICoreWebView2CompositionController,
at: vs_humanize::Point,
) -> EngineResult<()> {
wv2_send_mouse(comp, COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN, at)?;
std::thread::sleep(Duration::from_millis(15));
wv2_send_mouse(comp, COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP, at)?;
std::thread::sleep(Duration::from_millis(30));
Ok(())
}
mod wv2_cookies {
use std::sync::mpsc;
use webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2, ICoreWebView2Cookie, ICoreWebView2CookieList, ICoreWebView2CookieManager,
ICoreWebView2_2, COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX,
COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE, COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT,
};
use webview2_com::{pwstr_from_str, take_pwstr, GetCookiesCompletedHandler};
use windows::core::{Interface, BOOL, HSTRING, PCWSTR, PWSTR};
use crate::backend::auth::CookieData;
use crate::engine::{EngineError, EngineResult};
fn cookie_manager(web_view: &ICoreWebView2) -> EngineResult<ICoreWebView2CookieManager> {
let v2: ICoreWebView2_2 = web_view
.cast()
.map_err(|e| EngineError::Other(format!("cast to ICoreWebView2_2: {e}")))?;
unsafe { v2.CookieManager() }.map_err(|e| EngineError::Other(format!("CookieManager: {e}")))
}
pub(super) fn get_all_cookies(web_view: &ICoreWebView2) -> EngineResult<Vec<CookieData>> {
let manager = cookie_manager(web_view)?;
let (tx, rx) = mpsc::channel();
let empty: HSTRING = HSTRING::new();
let manager_for_init = manager.clone();
GetCookiesCompletedHandler::wait_for_async_operation(
Box::new(move |handler| {
unsafe { manager_for_init.GetCookies(PCWSTR(empty.as_ptr()), &handler) }
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |hr, list| {
hr?;
let mut out: Vec<CookieData> = Vec::new();
if let Some(list) = list {
if let Ok(count) = read_count(&list) {
for i in 0..count {
if let Ok(c) = unsafe { list.GetValueAtIndex(i) } {
out.push(serialize(&c));
}
}
}
}
let _ = tx.send(out);
Ok(())
}),
)
.map_err(|e| EngineError::Other(format!("GetCookies: {e:?}")))?;
rx.recv()
.map_err(|_| EngineError::Other("GetCookies: channel closed".into()))
}
pub(super) fn set_cookies(
web_view: &ICoreWebView2,
cookies: &[CookieData],
) -> EngineResult<()> {
let manager = cookie_manager(web_view)?;
for c in cookies {
if c.name.is_empty() || c.domain.is_empty() {
continue;
}
let name = pwstr_from_str(&c.name);
let value = pwstr_from_str(&c.value);
let domain = pwstr_from_str(&c.domain);
let path_str = if c.path.is_empty() {
"/"
} else {
c.path.as_str()
};
let path = pwstr_from_str(path_str);
let cookie: ICoreWebView2Cookie = unsafe {
manager.CreateCookie(
PCWSTR(name.0),
PCWSTR(value.0),
PCWSTR(domain.0),
PCWSTR(path.0),
)
}
.map_err(|e| EngineError::Other(format!("CreateCookie: {e}")))?;
unsafe { cookie.SetIsHttpOnly(c.http_only) }
.map_err(|e| EngineError::Other(format!("SetIsHttpOnly: {e}")))?;
unsafe { cookie.SetIsSecure(c.secure) }
.map_err(|e| EngineError::Other(format!("SetIsSecure: {e}")))?;
#[allow(clippy::cast_precision_loss)]
if let Some(unix) = c.expires_unix {
unsafe { cookie.SetExpires(unix as f64) }
.map_err(|e| EngineError::Other(format!("SetExpires: {e}")))?;
}
if let Some(ss) = c.same_site.as_deref() {
let kind = match ss {
"Strict" => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT,
"None" => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE,
_ => COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX,
};
unsafe { cookie.SetSameSite(kind) }
.map_err(|e| EngineError::Other(format!("SetSameSite: {e}")))?;
}
unsafe { manager.AddOrUpdateCookie(&cookie) }
.map_err(|e| EngineError::Other(format!("AddOrUpdateCookie: {e}")))?;
let _ = take_pwstr(name);
let _ = take_pwstr(value);
let _ = take_pwstr(domain);
let _ = take_pwstr(path);
}
Ok(())
}
fn read_count(list: &ICoreWebView2CookieList) -> windows::core::Result<u32> {
let mut count: u32 = 0;
unsafe { list.Count(&raw mut count) }?;
Ok(count)
}
fn read_pwstr_getter<F>(call: F) -> String
where
F: FnOnce(&mut PWSTR) -> windows::core::Result<()>,
{
let mut out: PWSTR = PWSTR::null();
match call(&mut out) {
Ok(()) if !out.is_null() => take_pwstr(out),
_ => String::new(),
}
}
fn read_bool_getter<F>(call: F) -> bool
where
F: FnOnce(&mut BOOL) -> windows::core::Result<()>,
{
let mut out: BOOL = BOOL(0);
match call(&mut out) {
Ok(()) => out.as_bool(),
Err(_) => false,
}
}
fn serialize(c: &ICoreWebView2Cookie) -> CookieData {
let name = read_pwstr_getter(|p| unsafe { c.Name(&raw mut *p) });
let value = read_pwstr_getter(|p| unsafe { c.Value(&raw mut *p) });
let domain = read_pwstr_getter(|p| unsafe { c.Domain(&raw mut *p) });
let path = read_pwstr_getter(|p| unsafe { c.Path(&raw mut *p) });
let secure = read_bool_getter(|b| unsafe { c.IsSecure(&raw mut *b) });
let http_only = read_bool_getter(|b| unsafe { c.IsHttpOnly(&raw mut *b) });
let expires_unix = {
let mut d: f64 = 0.0;
match unsafe { c.Expires(&raw mut d) } {
Ok(()) if d > 0.0 => {
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
let i = d.round() as i64;
Some(i)
}
_ => None,
}
};
let same_site = {
let mut k = COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX;
match unsafe { c.SameSite(&raw mut k) } {
Ok(()) => match k {
COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT => Some("Strict".to_string()),
COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE => Some("None".to_string()),
COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX => Some("Lax".to_string()),
_ => None,
},
_ => None,
}
};
CookieData {
name,
value,
domain,
path,
expires_unix,
secure,
http_only,
same_site,
}
}
}